General Guide: See Global LCP Guide for universal concepts and fixes.
What is LCP?
Largest Contentful Paint measures when the largest content element becomes visible. Google recommends LCP under 2.5 seconds. Bloomreach Experience Manager (brXM) uses a headless SPA architecture that adds hydration overhead to every page load.
Bloomreach-Specific LCP Causes
Bloomreach brXM delivers content through a headless API consumed by a frontend SPA (typically React or Next.js). LCP problems stem from:
- SPA hydration delay -- the JavaScript bundle must download, parse, and hydrate before any content renders, pushing LCP to 3-6 seconds on mobile
- Delivery API waterfall -- the frontend fetches the page model from the Delivery API, then fetches individual component data, creating sequential network requests
- Unoptimized DAM images -- images from Bloomreach's content repository served without CDN transforms or responsive variants
- Experience Manager preview overhead -- the Channel Manager preview JavaScript bundle (~200KB) accidentally included in production builds
- Personalization flicker -- Bloomreach Engagement (formerly Exponea) swaps content client-side after initial render
Fixes
1. Enable Server-Side Rendering or Static Generation
The single biggest LCP improvement for Bloomreach is moving from client-side SPA rendering to SSR or SSG:
// Next.js example with Bloomreach SDK
// pages/[[...path]].tsx
import { BrPage } from '@bloomreach/react-sdk';
import axios from 'axios';
export async function getServerSideProps(context) {
const configuration = {
endpoint: process.env.BRXM_ENDPOINT,
path: `/${(context.params.path || []).join('/')}`,
};
// Fetch page model server-side -- eliminates client waterfall
const pageModel = await axios.get(
`${configuration.endpoint}/pages${configuration.path}`
);
return {
props: { configuration, pageModel: pageModel.data },
};
}
This eliminates the client-side API waterfall that adds 500-1500ms to LCP.
2. Optimize Delivery API Responses
Reduce payload size by requesting only needed fields:
// Use field selection to reduce API response size
const apiUrl = `${BRXM_ENDPOINT}/pages/homepage?_fields=page,components.main`;
// Cache API responses at the CDN edge
// In your CDN config (e.g., Akamai, CloudFront):
// Cache-Control: public, max-age=300, s-maxage=3600
// Vary: Accept-Encoding
Configure Bloomreach's delivery tier caching in delivery-tier.yaml:
# delivery-tier.yaml
caching:
pageModelApi:
enabled: true
maxAge: 3600 # Cache page models for 1 hour
staleWhileRevalidate: 86400
resourceApi:
enabled: true
maxAge: 86400 # Cache static resources for 24 hours
3. Optimize Image Delivery from Bloomreach DAM
Bloomreach stores images in its content repository. Use image URL manipulation to serve optimized variants:
// Bloomreach image URL helper with CDN transforms
function getOptimizedImageUrl(imageRef, width = 800) {
const baseUrl = imageRef?.url || '';
// Append Bloomreach image processing parameters
return `${baseUrl}?width=${width}&quality=80&format=webp`;
}
// In your React component
function HeroBanner({ document }) {
const image = document.getData().image;
return (
<img
src={getOptimizedImageUrl(image, 1920)}
srcSet={`
${getOptimizedImageUrl(image, 640)} 640w,
${getOptimizedImageUrl(image, 1024)} 1024w,
${getOptimizedImageUrl(image, 1920)} 1920w
`}
sizes="100vw"
width="1920"
height="600"
alt={image?.alt || ''}
loading="eager"
fetchPriority="high"
/>
);
}
4. Remove Channel Manager Preview from Production
The brXM Channel Manager preview bundle is sometimes accidentally included in production:
// Check your _app.tsx or layout component
// WRONG: Always loading preview
import { BrPage } from '@bloomreach/react-sdk';
// RIGHT: Conditionally load preview only in CMS context
const isPreview = typeof window !== 'undefined' &&
window.location.search.includes('token=');
// Only include Channel Manager scripts in preview mode
{isPreview && (
<script src={`${BRXM_ENDPOINT}/autoreload`} />
)}
5. Preload Hero Assets
Add preload hints for the LCP element:
<!-- In your document head (Next.js _document.tsx or equivalent) -->
<link
rel="preload"
as="image"
href="/api/image/hero-banner.webp?width=1920&quality=80"
type="image/webp"
/>
<!-- Preconnect to Bloomreach API -->
<link rel="preconnect" href="https://your-brxm-instance.bloomreach.io" />
<link rel="dns-prefetch" href="https://your-brxm-instance.bloomreach.io" />
Measuring LCP on Bloomreach
- Separate SPA hydration from content render -- use Chrome DevTools Performance tab to see when React hydration completes vs. when the LCP image appears
- Test with SSR disabled -- compare LCP with SSR on vs. client-only to quantify the hydration cost
- Bloomreach Dashboard -- check the delivery tier metrics for API response times (target under 100ms)
- Test page types: landing pages (heavy with components), product detail pages, blog posts, and search results pages all have different API call patterns
Analytics Script Impact
Bloomreach Engagement (the analytics/personalization side) adds its own tracking script:
- Exponea SDK (~80KB) -- loads asynchronously but can block LCP if it triggers content personalization that swaps hero elements
- Server-side personalization -- use Bloomreach's server-side decisioning via the Delivery API instead of client-side swaps to avoid LCP regression
- GTM integration -- if using GTM alongside Bloomreach tracking, ensure GTM loads with
asyncand fires Bloomreach events via dataLayer rather than loading both SDKs independently