Fragment Batching — Complete Guide
In this tutorial, you will learn about Fragment Batching. We cover key concepts, practical examples, and best practices to help you master this topic.
Fragment batching uses DocumentFragment to batch multiple DOM insertions into a single reflow, dramatically improving performance for bulk content rendering.
What You'll Learn
- How to batch large DOM insertions with DocumentFragment
- How to batch attribute and style changes
- How to use off-DOM manipulation (detach, modify, reattach)
- Advanced patterns for high-performance DOM updates
Why It Matters
Every DOM change costs time. Batching reduces the number of reflows from hundreds to just one. For data grids, Infinite Scroll feeds, and real-time dashboards, fragment batching is essential for maintaining 60fps performance.
Real-World Use
- A real-time stock ticker batches price updates into fragments every 100ms
- A social media feed renders new posts in fragments
- A data export tool builds large tables in fragments before insertion
flowchart LR A[Multiple DOM Changes] --> B[Use DocumentFragment] A --> C[Off-DOM Manipulation] A --> D[Batch with rAF] B --> E[Append all to fragment] E --> F[Single DOM insertion] C --> G[Detach parent] G --> H[Modify detached tree] H --> I[Reattach parent] D --> J[Collect changes] J --> K[Apply in rAF callback]
Fragment Batching for Large Datasets
When rendering many items, use fragments to avoid individual DOM insertions.
function renderLargeList(container, items) {
// Use a document fragment for bulk insertion
const fragment = document.createDocumentFragment();
for (const item of items) {
const div = document.createElement('div');
div.className = 'list-item';
div.textContent = `${item.name} - ${item.description}`;
// Optional: add data attributes
div.dataset.id = item.id;
fragment.appendChild(div);
}
// Single DOM append
container.appendChild(fragment);
console.log(`Rendered ${items.length} items in batch`);
}
// Generate 5000 items
const data = Array.from({ length: 5000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
description: `Description for product ${i}`
}));
const container = document.querySelector('#product-list');
console.time('batch-render');
renderLargeList(container, data);
console.timeEnd('batch-render');
// Without fragment: ~200-500ms
// With fragment: ~20-50ms
Expected output: The container shows all 5000 items. The batch render completes significantly faster than individual appends would.
Batching Attribute and Style Changes
Use CSS classes and style property setting in bulk to minimize reflows.
function batchUpdateStyles(elements, styles) {
// BAD: set each style individually per element
// elements.forEach(el => {
// el.style.color = styles.color;
// el.style.backgroundColor = styles.bg;
// el.style.padding = styles.padding;
// });
// GOOD: create a CSS class and toggle it
const styleSheet = document.createElement('style');
styleSheet.textContent = `
.batch-updated {
color: ${styles.color} !important;
background-color: ${styles.bg} !important;
padding: ${styles.padding} !important;
}
`;
document.head.appendChild(styleSheet);
// Apply the class to all elements (single reflow)
elements.forEach(el => el.classList.add('batch-updated'));
console.log(`Updated ${elements.length} elements via CSS class`);
}
// Alternative: set styles on a parent (cascading)
function batchUpdateViaParent(parent, styles) {
parent.style.color = styles.color;
parent.style.backgroundColor = styles.bg;
// Children inherit color, saving individual operations
}
Expected output: Adding a CSS class to many elements is faster than setting inline styles individually. The class approach triggers one style recalculation per element per class addition, but the browser optimizes this better than multiple inline style changes.
Off-DOM Manipulation
For massive changes, remove the container, modify it, and reattach.
function massiveTableUpdate(tableId, newData) {
const table = document.getElementById(tableId);
// Step 1: Detach from DOM
const parent = table.parentNode;
const nextSibling = table.nextSibling;
parent.removeChild(table);
console.log('Table detached from DOM');
// Step 2: Modify while detached (zero reflows)
const tbody = table.querySelector('tbody');
tbody.innerHTML = ''; // Clear (no reflow — detached!)
// Build new rows
const fragment = document.createDocumentFragment();
newData.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.textContent = cell;
tr.appendChild(td);
});
fragment.appendChild(tr);
});
tbody.appendChild(fragment);
// Step 3: Reattach to DOM (single reflow)
if (nextSibling) {
parent.insertBefore(table, nextSibling);
} else {
parent.appendChild(table);
}
console.log('Table reattached after update');
}
// This technique limits reflows to exactly 2:
// 1. When detaching (removing from layout)
// 2. When reattaching
// All intermediate changes cost zero reflows
Expected output: The table updates smoothly without jank. Even with thousands of rows, the operation is fast because all DOM modifications happen off-screen.
Batching with requestAnimationFrame
Combine rAF with fragment batching for real-time updates.
class BatchedUpdater {
constructor(container) {
this.container = container;
this.pendingItems = [];
this.rafId = null;
}
addItem(item) {
this.pendingItems.push(item);
this.scheduleFlush();
}
addItems(items) {
this.pendingItems = this.pendingItems.concat(items);
this.scheduleFlush();
}
scheduleFlush() {
if (!this.rafId) {
this.rafId = requestAnimationFrame(() => this.flush());
}
}
flush() {
if (this.pendingItems.length === 0) {
this.rafId = null;
return;
}
const fragment = document.createDocumentFragment();
while (this.pendingItems.length > 0) {
const item = this.pendingItems.shift();
const element = this.createItemElement(item);
fragment.appendChild(element);
}
this.container.appendChild(fragment);
this.rafId = null;
console.log('Flushed batch to DOM');
}
createItemElement(item) {
const div = document.createElement('div');
div.className = 'batched-item';
div.textContent = item.text;
return div;
}
destroy() {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.pendingItems = [];
}
}
// Usage: updates are batched per animation frame
const updater = new BatchedUpdater(document.querySelector('.feed'));
// Simulate rapid additions
for (let i = 0; i < 100; i++) {
updater.addItem({ text: `Item ${i}` });
}
// All 100 items are inserted in a single rAF callback
Expected output: All 100 items appear at once in the next animation frame. If items are added incrementally, they batch into the next rAF callback. No intermediate reflows occur.
Batch Removal Pattern
Removing many elements also benefits from batching.
function batchRemove(container, selector) {
// Find all elements to remove
const toRemove = container.querySelectorAll(selector);
console.log(`Removing ${toRemove.length} elements`);
if (toRemove.length === 0) return;
// Collect into fragment (which removes from parent)
const fragment = document.createDocumentFragment();
// But fragment.appendChild removes from current parent...
// Actually, just iterate and remove efficiently
// Efficient approach: detach container, remove, reattach
const parent = container.parentNode;
const nextSibling = container.nextSibling;
parent.removeChild(container);
// Now remove from detached container (no reflow)
toRemove.forEach(el => el.remove());
// Reattach
if (nextSibling) {
parent.insertBefore(container, nextSibling);
} else {
parent.appendChild(container);
}
// Alternative: swap with a clone
// const clone = container.cloneNode(true);
// Remove elements from clone...
// parent.replaceChild(clone, container);
}
Expected output: Elements are removed in a single visual update. The detach-remove-reattach pattern limits reflows to two regardless of how many elements are removed.
Common Mistakes
- Using a fragment when the container is already detached — If the container is off-DOM, you do not need a fragment. Direct appends cost no reflow until reattachment.
- Forgetting that fragment.appendChild removes from old parent — If A is a child of B and you do fragment.appendChild(A), A is removed from B. This is useful for moving elements but can be surprising.
- Over-optimizing small batches — Fragment batching overhead is not worth it for 1-5 elements. Use it for batches of 10+ elements.
- Not cleaning up rAF IDs — If the component unmounts before the scheduled rAF fires, the callback still executes. Cancel the rAF in cleanup.
- Batching in scenarios with priority updates — If some updates are urgent (user input response), flush the batch immediately instead of waiting for rAF.
Practice Questions
- What is the primary benefit of DocumentFragment for batch operations? It allows building a subtree in memory and inserting it all at once, triggering a single reflow instead of one per node.
- Why does detaching a container improve performance for large updates? All DOM changes on the detached subtree cause zero reflows. Only the detachment and reattachment trigger reflows.
- How does requestAnimationFrame help with batching? It aligns the batch flush with the browser's rendering cycle (every ~16ms), allowing multiple changes to accumulate before a single visual update.
- Challenge: Build a logging dashboard that receives 100 log messages per second. Use fragment batching and rAF to display them smoothly. Log messages should accumulate for 100ms, then render as a batch. Show the current batch size on screen.
FAQ
Mini Project
Create a comparison demo that renders 2000 items using three methods: 1) Individual appendChild in a loop, 2) innerHTML string building, 3) DocumentFragment batching. Show execution time for each. Also implement a "live updates" mode that adds 10 items per second using rAF batching vs direct insertion. Display FPS counter to demonstrate the performance difference.
What's Next
Continue with Lesson 25: Virtual Scrolling to learn how to render only visible items in large lists for maximum performance.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro