Headless Bitrix Image Pipeline: What Breaks First in Next.js
We launched headless Bitrix + Next.js. LCP got worse.
Not by a little — 2.4 seconds instead of 1.6 on the old monolith. The CDN was running. Next.js Image was in the code. Everything looked fine.
Three hours later we found it: every product image was routing through the PHP server because we hadn't added remotePatterns in next.config.js. Next.js didn't know that /upload/resize_cache/... was a static file. It treated it like any external URL and proxied it through /_next/image. Bitrix resized it. Next.js resized it again. The CDN wasn't involved at all.
The fix took 20 minutes. The diagnosis took three hours.
Here's what actually happens with images in headless Bitrix — and where things break.
How Bitrix serves images: the part nobody explains before you ship
Bitrix stores original uploads in /upload/ — a real directory on disk at <site_root>/upload/iblock/abc/original.jpg.
Resized variants are generated on-demand by iResize and stored in /upload/resize_cache/. The path looks like /upload/resize_cache/iblock/abc_600x400_exact/original.jpg. The second request for the same dimensions hits the cached file; PHP just serves it directly, or nginx handles it as static.
On a traditional Bitrix monolith, nginx serves /upload/ as static. PHP doesn't touch it if the file exists.
In a headless setup, this breaks down fast. Next.js Image, by default, proxies external images through its /_next/image handler. Without explicitly listing the Bitrix domain in remotePatterns, every <Image src="https://bitrix.example.com/upload/..."> goes through Next.js — which fetches the image from Bitrix, resizes it for the current viewport, and serves the result.
Bitrix already resized it. PHP already processed it. And now Next.js does it again.
The /upload/ trap: why Next.js Image routes your images through PHP
Without a remotePatterns entry for your Bitrix domain, Next.js Image proxies every /upload/ request through its own /_next/image handler instead of serving the file directly — doubling processing and bypassing your CDN.
The fix is a two-line config change. But it's easy to miss if you're focused on API integration and not thinking about media.
// next.config.js
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'bitrix.example.com',
pathname: '/upload/**',
},
],
}
This tells Next.js to allow direct image loading from your Bitrix domain. Now <Image> components with Bitrix URLs serve the image directly from Bitrix's CDN (or nginx), without the double-proxy.
But there's a second part: make sure Bitrix's nginx config actually serves /upload/ as static — without involving PHP-FPM. Some server configurations, particularly ISPmanager setups, have a catch-all location ~ \.php$ without a proper /upload/ exclusion. When that happens, requests to /upload/resize_cache/ go through FPM.
On a 28k SKU catalog, this added around 15-20% unnecessary load to the PHP pool — for file-serving that nginx should handle alone.
resize_cache cold starts and what they cost you on launch day
iResize generates resized variants lazily: the first request for a given dimensions creates the file, subsequent requests serve the cached result.
On a Bitrix monolith, this is mostly invisible. The server renders the page, triggers iResize for the product image dimensions, and by the time the browser requests the image, it's already cached.
In a headless setup, the timing is different. Next.js gets PREVIEW_PICTURE from the Bitrix REST API — that's a URL to whatever Bitrix has stored. If you request a specific size that doesn't exist yet in /upload/resize_cache/, the first HTTP request to that URL triggers iResize. PHP opens the original, resizes it, saves the result. That takes 200-600ms depending on the original file size.
On a 28k SKU catalog, a fresh deployment or a resize_cache clear means hundreds of cold resizes in the first minutes of traffic. LCP on listing pages spikes noticeably.
Two practical approaches: a warm-up script that walks your sitemap after deployment and pre-requests each image URL at the sizes your frontend uses; or a CDN with sufficient TTL on /upload/resize_cache/ so cold starts only happen once per image, not once per CDN node.
CDN in front of Bitrix: three rules, one config mistake to avoid
CDN in front of Bitrix is standard for headless projects. But the caching policy for /upload/ needs to be deliberate.
First — /upload/resize_cache/** can and should be aggressively cached. TTL of several hours to several days is fine. Bitrix doesn't automatically invalidate these URLs when the source image changes: the resize stays in cache until you clear it explicitly. If a product image changes in Bitrix, the resize URL stays the same — only the original changes. For forced updates, either clear resize_cache or version the URL.
Second — /upload/ originals need a more cautious policy. They can be updated. Cache them too aggressively and users see stale images after product photo updates.
Third — PHP should never handle requests to /upload/. Double-check your nginx config: location /upload/ should be served as static files (try_files $uri =404), with no fastcgi_pass. Moving static file serving off the PHP layer on the 28k SKU project cut approximately 40% of app-server CPU load.
The common mistake is assuming "CDN is in front, so Bitrix is protected." That's only true if nginx stops PHP from handling static file requests. If it doesn't, CDN misses flow straight to PHP, and a spike in cache misses is a spike in FPM usage.
Open Graph images in headless: a separate pipeline you didn't plan for
When Next.js renders <meta property="og:image">, it typically uses PREVIEW_PICTURE or DETAIL_PICTURE from the Bitrix REST API. That's a direct URL to an image on the Bitrix server.
The catch: social parsers — Facebook, LinkedIn, Telegram, Slack — fetch OG images directly. They don't go through Next.js Image optimization. They expect a ready JPEG or PNG at a direct URL.
If your CDN doesn't cover that URL, every time a product page gets shared, there's a direct request to Bitrix's PHP server. At scale or during a viral share, this shows up in your server metrics.
The fix: make sure OG image URLs go through your CDN. If Bitrix returns absolute URLs (https://bitrix.example.com/upload/...), verify the CDN covers that path and caches it. If it returns relative paths, assemble the absolute URL in Next.js before writing it to og:image.
One more thing: OG images should be at least 1200×630px. Bitrix returns the resize dimensions you request via iResize parameters. If you haven't explicitly requested the right size, Facebook or LinkedIn might receive a small thumbnail or a raw 4K original.
Pre-launch checklist for your Bitrix image pipeline
Before shipping headless Bitrix + Next.js:
next.config.js:remotePatternsincludes your Bitrix domain with/upload/**pattern- Nginx on Bitrix server:
/upload/served as static — no PHP-FPM involved - CDN:
/upload/resize_cache/**cached with explicit TTL - CDN:
/upload/originals — caching policy checked, doesn't conflict with image updates - Warm-up script: runs after each deployment of new content
- OG images: dimensions ≥ 1200×630px, absolute URL, CDN path covers it
- Post-deploy monitoring: LCP on listing pages — first thing to check after going live
To verify: Chrome DevTools → Network → Images. Look for requests to /upload/, confirm Cache-Control isn't no-cache, check for X-Cache: HIT from your CDN.
The image pipeline in headless Bitrix isn't complicated. But it needs to be designed deliberately — it doesn't "just work" when you add <Image>.
Most of the LCP regressions I've seen in headless Bitrix projects come from here, not from the API layer.
For other production surprises in headless Bitrix REST API integration, see this article. For how Next.js ISR handles cache invalidation when Bitrix updates a product, see this one.