. DPU provides two patterns:
- Range replacement (
<?start name="..."> / <?end>): Wraps placeholder content that will be entirely replaced later.
- Insertion marker (
<?marker name="...">): Defines a precise insertion point for streamed content.
<main class="dashboard-shell">
<header class="nav-bar">
<h1>System Overview</h1>
</header>
<section class="metrics-panel">
<h2>Live Analytics</h2>
<?start name="analytics-feed">
<div class="skeleton-loader" aria-busy="true">
<p>Waiting for data stream...</p>
</div>
<?end>
</section>
<aside class="quick-actions">
<?marker name="action-buttons">
</aside>
</main>
Step 2: Stream Replacement Fragments
When the backend finishes processing slow queries, it pushes HTML fragments wrapped in a <template> element with a for attribute matching the declared name. The browser parses the stream, locates the corresponding placeholder, and performs the DOM update automatically.
<template for="analytics-feed">
<div class="metrics-grid">
<div class="metric-card">
<span class="value">14,203</span>
<span class="label">Active Sessions</span>
</div>
<div class="metric-card">
<span class="value">89.4%</span>
<span class="label">Uptime</span>
</div>
</div>
</template>
<template for="action-buttons">
<button type="button" data-action="export">Export Report</button>
<button type="button" data-action="refresh">Refresh Data</button>
</template>
Step 3: Handle JavaScript Integration (Optional)
DPU is declarative, but modern applications still require JavaScript for event delegation, analytics tracking, or framework hydration. The new HTML insertion APIs introduced alongside DPU provide consistent, secure methods for programmatic updates when declarative streaming isn't sufficient.
interface StreamController {
target: HTMLElement;
fragment: string;
}
function applyPartialUpdate(payload: StreamController): void {
const { target, fragment } = payload;
// Replaces inner content safely, consistent across browsers
target.setHTML(fragment);
// Re-attach event listeners or hydrate framework components
initializeInteractiveElements(target);
}
Architecture Decisions & Rationale
- Why use
<?start>/<?end> instead of <?marker>? Range replacement preserves accessibility states (like aria-busy) during loading and ensures the DOM structure remains predictable. Markers are better suited for appending sequential items (e.g., chat messages, log entries) where no placeholder exists.
- Why not replace all client-side rendering? DPU targets non-interactive, server-generated content. Forms, modals, drag-and-drop interfaces, and real-time collaborative features require client-side state management. DPU complements these by offloading static or slowly resolving HTML.
- Why introduce
setHTML()? Traditional methods like innerHTML, outerHTML, and insertAdjacentHTML() have inconsistent behavior regarding script execution, sanitization, and Trusted Types compliance. setHTML() standardizes replacement semantics, runs through the browser's security pipeline, and provides predictable DOM mutation events.
- Streaming strategy: Chunk the initial payload to deliver critical layout and navigation first. Queue slow endpoints in a background worker or server-side stream. Push
<template> fragments as they resolve. This prevents waterfall delays and improves First Contentful Paint (FCP).
Pitfall Guide
1. Region Naming Collisions
Explanation: DPU matches <template for="..."> to placeholders by exact string match. Duplicate names across different sections cause unpredictable replacements or silent failures.
Fix: Namespace regions using a consistent prefix (e.g., dashboard-analytics-feed, profile-recommendations). Validate names during build time with a linter rule that checks for duplicates in the HTML stream.
2. Framework Hydration Conflicts
Explanation: React, Vue, or Svelte hydration expects a stable DOM structure. If DPU replaces nodes after hydration completes, the framework's virtual tree becomes desynchronized, causing missing event listeners or stale state.
Fix: Isolate DPU regions from framework-managed trees. Use data-dpu-region attributes to mark boundaries, and disable hydration for those subtrees. Alternatively, trigger framework re-renders explicitly after DPU updates using lifecycle hooks or custom events.
3. Overstreaming Large Templates
Explanation: Sending massive HTML fragments in a single chunk blocks the parser, increases memory pressure, and delays rendering. Browsers process <template> content synchronously during stream parsing.
Fix: Chunk large responses into logical segments (e.g., 50β100 items per template). Use <?marker> for incremental appends instead of replacing entire lists. Monitor stream backpressure and pause transmission if the main thread exceeds 50ms processing time.
4. Assuming Automatic Security/Sanitization
Explanation: DPU does not sanitize HTML. Malicious or malformed fragments can inject scripts, break layout, or trigger XSS if the backend trusts unvalidated user input.
Fix: Implement server-side sanitization using libraries like DOMPurify or framework-specific escape utilities. Validate fragment structure before streaming. Enable Trusted Types in your Content Security Policy to enforce programmatic HTML creation rules.
5. Ignoring Fallback Strategies
Explanation: DPU is experimental and limited to Chrome 148+. Relying on it exclusively breaks functionality in Firefox, Safari, or older Chromium versions.
Fix: Implement a progressive enhancement pattern. Detect DPU support via feature detection ('HTMLTemplateElement' in window && 'for' in HTMLTemplateElement.prototype). Fall back to traditional fetch() + setHTML() or framework rendering when unsupported.
6. Misusing Markers for Replacement
Explanation: <?marker> inserts content at a precise point but does not remove existing nodes. Using it to replace loading states leaves duplicate DOM elements and breaks layout flow.
Fix: Reserve <?marker> for append-only scenarios. Use <?start>/<?end> when you need to swap placeholder content. Document the intended behavior in your streaming architecture guide to prevent team confusion.
7. Blocking the Main Thread with Synchronous Streaming
Explanation: If the server pushes templates faster than the browser can parse and insert them, the main thread becomes saturated, causing jank and delayed user input handling.
Fix: Implement server-side pacing. Use ReadableStream with backpressure handling. Throttle template emission based on requestIdleCallback or performance observer metrics. Prioritize critical UI regions over secondary content.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Static dashboard with slow analytics API | DPU Streaming (<?start>/<?end>) | Eliminates client JSON parsing, improves FCP, reduces JS payload | Low infrastructure cost, moderate backend refactoring |
| Real-time chat or collaborative editor | Client-Side State + setHTML() | Requires immediate interactivity, conflict resolution, and offline support | Higher client complexity, minimal server changes |
| E-commerce product recommendations | DPU Streaming (<?marker> append) | Allows incremental loading without blocking initial render, preserves scroll position | Low cost, improves perceived performance |
| Legacy SPA with heavy hydration | Framework-native rendering + lazy loading | DPU integration would require extensive DOM isolation and fallback logic | High refactoring cost, defer until architecture modernization |
Configuration Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Streaming Dashboard</title>
<style>
.skeleton-loader { opacity: 0.6; animation: pulse 1.5s infinite; }
@keyframes pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.8; } }
.metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
</style>
</head>
<body>
<div id="app-shell">
<header>Navigation Shell</header>
<main>
<section id="primary-content">
<h2>System Metrics</h2>
<?start name="metrics-region">
<div class="skeleton-loader">Loading metrics...</div>
<?end>
</section>
<aside id="secondary-content">
<?marker name="notifications-feed">
</aside>
</main>
</div>
<script type="module">
// Feature detection & fallback
const supportsDPU = 'HTMLTemplateElement' in window &&
'for' in HTMLTemplateElement.prototype;
if (!supportsDPU) {
console.warn('DPU not supported. Falling back to client-side fetch.');
fetch('/api/metrics')
.then(res => res.text())
.then(html => {
const target = document.querySelector('#metrics-region');
if (target) target.setHTML(html);
});
}
// Event delegation for streamed content
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (btn) {
console.log(`Action triggered: ${btn.dataset.action}`);
}
});
</script>
</body>
</html>
Quick Start Guide
- Identify a slow endpoint: Pick a component that currently fetches JSON and renders static HTML (e.g., a recommendations list or analytics widget).
- Wrap the placeholder: Replace the loading state with
<?start name="your-region"> and <?end> in your server-rendered HTML template.
- Stream the fragment: Modify your backend to push
<template for="your-region"> containing the final HTML once the data resolves. Use chunked transfer encoding or server-sent events.
- Verify in Chrome 148+: Open DevTools, monitor the Network tab for streaming chunks, and confirm the browser automatically replaces the placeholder without JavaScript intervention.
- Add fallback logic: Implement feature detection and a traditional
fetch() + setHTML() path for unsupported browsers to ensure universal compatibility.