Edge Side Includes vs Client-Side Fragments: Where to Assemble the Page
This comparison sits within Edge Compute & Dynamic Caching, part of Advanced Caching Strategies & CDN Architecture. Once a page is split into a cacheable shell and fragments that cannot share its cache lifetime — a personal header, a live stock badge, a recommendation rail — something has to put them back together. There are two places to do it: at the edge, before the HTML reaches the browser (Edge Side Includes, or an edge function streaming fragments into the shell), or in the browser, after the shell has painted (JavaScript fetching fragments and inserting them).
Both keep the shell cached. They differ in what the first paint contains, how failures behave, and which Core Web Vitals they affect. Edge assembly produces a complete first paint but makes the response depend on fragment latency. Client assembly makes the first paint fast and independent of fragments, at the cost of a later visual completion and layout-shift risk.
Rapid Diagnosis
- Identify fragments and their latency. Measure each fragment endpoint's p75 and p95 response time from the edge and from clients.
- Check whether fragments are in the LCP region. A fragment that contains or precedes the LCP element is a strong candidate for edge assembly.
- Check layout-shift risk. Client-inserted fragments above other content shift layout unless space is reserved.
- Check streaming support. Edge assembly only keeps TTFB low if the platform streams the shell while fragments load.
Root Cause Analysis
1. Fragment latency becomes page latency at the edge. With classic ESI, the edge waits for each include before sending what follows; a slow fragment delays everything after it.
2. Late insertion causes CLS on the client. Fragments inserted into unreserved space push content down after the user has started reading.
3. Client fragments depend on JavaScript. If the script fails or is delayed by main-thread work, the fragment appears late or not at all.
4. Duplicate work. Some setups render fragments at the edge and then re-fetch them on the client for hydration, paying twice.
Step-by-Step: Choosing and Implementing
1. Classify fragments by position and latency
Fragments in the first viewport that affect layout (a personalised banner above the hero) favour edge assembly. Fragments below the fold or in fixed-size slots (cart count in the header, recommendations rail) suit client assembly.
2. Stream edge-assembled fragments rather than blocking
// Edge function: stream the shell, fetch the fragment in parallel, slot it in.
const fragment = fetch(`${ORIGIN}/fragments/banner?u=${userId}`).then((r) => r.text());
return new HTMLRewriter()
.on('#personal-banner', { async element(el) { el.setInnerContent(await fragment, { html: true }); } })
.transform(cachedShell);
// trade-off: the stream pauses at the slot until the fragment resolves, so
// content after the slot is delayed by the fragment's latency. Place slots late
// in the document, or set a timeout and leave the default content.
Expected outcome: shell bytes before the slot arrive at cache speed; only content after the slot waits.
3. Insert client fragments into reserved space
const slot = document.querySelector('[data-fragment="recs"]'); // has min-height in CSS
fetch('/fragments/recs', { credentials: 'same-origin' })
.then((r) => r.text())
.then((html) => { slot.innerHTML = html; })
.catch(() => slot.remove());
// trade-off: removing a failed slot shifts content below it. Keep the reserved
// space with a neutral message instead if the slot is above other content.
Expected outcome: no layout shift and a fast, fragment-independent first paint.
4. Set timeouts for edge fragments
Never let a slow fragment hold the page hostage. Give each edge fragment a budget (for example 150ms) and fall back to the default content or to client loading if it is exceeded.
Verification
Measure TTFB, LCP and CLS for each approach on a staging page. Edge assembly should show complete first paint and zero fragment-related CLS, with TTFB unaffected when streaming is configured correctly. Client assembly should show fast LCP and no CLS if space is reserved. Simulate a slow fragment (add a delay to the endpoint) and confirm the page degrades gracefully in both cases.
Worked Example: A Media Homepage
A news homepage had three fragments: a personal "Continue reading" strip near the top, a live breaking-news ticker, and a recommendations rail far down the page. The team assembled the "Continue reading" strip at the edge with a 120ms timeout (falling back to a generic "Popular now" strip), streamed the ticker into its slot, and fetched recommendations on the client into a fixed-height rail. TTFB stayed at cache-hit levels (p75 85ms), LCP was unaffected by fragment latency, and CLS from the old client-inserted top strip — 0.08 at p75 — disappeared.
Common Mistakes
- Blocking ESI on slow fragments. One slow include makes the entire page slow.
- Client-inserting above-the-fold content without reserved space. Guaranteed CLS.
- Caching fragments with the shell. Personal fragments must have their own (usually private or no) cache policy.
- No fallback. A failed fragment should show sensible default content, never a broken layout.
Edge Cases
ESI support varies. Akamai and Fastly support ESI natively; Cloudflare and others implement equivalents through edge functions. Behaviour around timeouts, nesting and caching of includes differs by vendor.
Hydration frameworks. If the page hydrates, fragments assembled at the edge must match what the client renders, or you create hydration mismatches. Treat fragment regions as non-hydrated islands where possible.
SEO. Edge-assembled content is visible to crawlers that do not run JavaScript; client fragments may not be. Keep SEO-relevant content in the shell or edge fragments.
Caching fragments themselves. Fragments can have their own cache entries (for example, a ticker cached for 30 seconds), which makes edge assembly even cheaper.
FAQ
Is ESI still relevant with modern edge functions?
The concept is; the syntax less so. Edge functions with streaming HTML rewriters provide the same assembly with more control over timeouts, parallelism and fallbacks. ESI remains convenient on CDNs that support it natively.
Which approach is better for LCP?
Whichever keeps fragments out of the LCP element's critical path. If a fragment is the LCP element, edge assembly with a strict timeout is better; otherwise client assembly keeps LCP independent of fragment latency.
Can I combine both?
Yes — most real pages do: edge assembly for the few fragments that shape the first viewport, client assembly for everything else.
How do fragments affect INP?
Client fragments add a fetch and a DOM insertion, usually small. Large fragments inserted during user interaction can cause presentation delay; insert them when idle or use content-visibility for heavy below-the-fold fragments.
What about micro-frontends?
Micro-frontend composition is fragment assembly at a larger scale. The same trade-offs apply, with the added cost that each fragment may bring its own JavaScript; composing at the edge keeps HTML fast but does not reduce that JavaScript.
How do I measure fragment impact in the field?
Mark fragment arrival with User Timing (or Element Timing on the fragment's main element) and send it with your RUM beacon alongside LCP and CLS. Comparing fragment arrival time with LCP shows whether fragments are on the critical path, and CLS attribution shows whether client insertions shift layout.
Related
- Personalizing cached pages at the edge — rewriting small personal details in the stream.
- Streaming HTML through CDNs and proxies — making sure the stream is not buffered.
- Fixing CLS from skeleton screens that resize — reserving space for client fragments.