Skip to content

Fragment Batching — Complete Guide

DodaTech Updated 2026-06-28 7 min read

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

  1. 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.
  2. 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.
  3. Over-optimizing small batches — Fragment batching overhead is not worth it for 1-5 elements. Use it for batches of 10+ elements.
  4. Not cleaning up rAF IDs — If the component unmounts before the scheduled rAF fires, the callback still executes. Cancel the rAF in cleanup.
  5. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

Is DocumentFragment supported in all browsers?

Yes. DocumentFragment is supported in all browsers, including very old ones.

Can I use a regular div as a fragment?

Yes, but a div is an actual element with style and layout implications. A DocumentFragment is lighter and does not affect the DOM tree.

Does fragment batching help with innerHTML operations?

Indirectly. Build the HTML string, assign innerHTML once. The browser parses it in one shot, which is similar to fragment batching.

What is the batch size threshold where fragments become beneficial?

Typically 10-20 elements. For fewer than 10, the fragment overhead may outweigh the benefit. Profile your specific use case.

Can I nest DocumentFragments?

Yes, but nested fragments flatten on insertion. The children of nested fragments move to the parent. This can be useful for building complex trees.

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