requestAnimationFrame (rAF) and requestIdleCallback (rIC) are browser scheduling APIs that give JavaScript access to the browser’s rendering and idle-time pipeline — letting you run code at the right moment in the browser’s event loop rather than flooding the main thread with synchronous work. requestAnimationFrame( callback ) schedules callback to run immediately before the next paint — the browser calls it once per frame at the display refresh rate (typically 60fps = every ~16.6ms, or 120fps = every ~8.3ms on high-refresh displays). This makes rAF the correct API for any DOM mutation intended to be visible as a smooth animation: reading layout properties and writing DOM changes inside a single rAF callback batches the read-layout and paint steps to avoid forced synchronous layout (the main cause of janky animations). requestIdleCallback( callback, { timeout } ) schedules callback during a browser idle period — a time slice when no user interaction, network activity, or pending frame renders are competing for the main thread. The callback receives an IdleDeadline object with a timeRemaining() method that returns how many milliseconds remain in the current idle period — work units should check deadline.timeRemaining() > 0 and yield back to the browser if the deadline is approaching, splitting large tasks into resumable chunks. The timeout option forces the callback to run after a maximum delay even if the browser never enters an idle state — critical for tasks that must eventually execute (like analytics beacons on page unload). In the WordPress admin context, rAF is the correct tool for admin UI animations (drag-and-drop, custom block editor transitions, progress bar updates) while rIC is ideal for non-urgent background tasks: prefetching menu data, initialising off-screen components, running spell-check, and serialising large form state to localStorage. The Web Workers post covered moving CPU-intensive work completely off the main thread; rAF and rIC improve scheduling of work that must remain on the main thread but can be deferred or split across frames.
Problem: A WordPress admin page renders a table of 2,000 rows by inserting all rows in a single synchronous loop — the browser freezes for 800ms, the page becomes unresponsive, and the scrollbar does not update until the loop completes. The table must be rendered client-side from a JSON API response because it supports live filtering.
Solution: Render the table in batches using requestIdleCallback with a deadline check — each idle callback renders as many rows as the idle budget allows and schedules the next batch, keeping the main thread available for user interactions throughout the render process.
// ── Idle-time batched rendering of large data sets ─────────────────────────────
/**
* Render items from `dataArray` into `container` using rIC to stay non-blocking.
* @param {HTMLElement} container
* @param {Array} dataArray
* @param {Function} renderItem — (item) => HTMLElement
*/
function renderInIdle(container, dataArray, renderItem) {
let index = 0;
const fragment = document.createDocumentFragment();
function renderBatch(deadline) {
// Keep rendering as long as idle time remains and data is available
while (index < dataArray.length && deadline.timeRemaining() > 1) {
fragment.appendChild(renderItem(dataArray[index]));
index++;
}
// Flush the fragment to the real DOM once per rIC call (one reflow)
if (fragment.childNodes.length > 0) {
container.appendChild(fragment.cloneNode(true));
// Clear the fragment for the next batch
while (fragment.firstChild) fragment.removeChild(fragment.firstChild);
}
// More data remains — schedule the next batch
if (index < dataArray.length) {
requestIdleCallback(renderBatch, { timeout: 500 });
}
}
// Start: force callback after 500ms even if browser is never truly idle
requestIdleCallback(renderBatch, { timeout: 500 });
}
// ── Usage: render WP admin inventory table from REST API ──────────────────────
async function initInventoryTable() {
const response = await fetch('/wp-json/myplugin/v1/inventory?per_page=2000', {
headers: { 'X-WP-Nonce': wpApiSettings.nonce },
});
const items = await response.json();
const tbody = document.querySelector('#inventory-table tbody');
renderInIdle(tbody, items, (item) => {
const tr = document.createElement('tr');
tr.innerHTML = `
${ escapeHtml(item.sku) }
${ escapeHtml(item.name) }
${ item.stock }
${ escapeHtml(item.price) }
`;
return tr;
});
}
// ── requestAnimationFrame: smooth progress bar update ────────────────────────
function animateProgressBar(barEl, targetPct) {
let currentPct = parseFloat(barEl.style.width) || 0;
const step = (targetPct - currentPct) / 30; // reach target over ~30 frames
function frame() {
currentPct = Math.min(targetPct, currentPct + step);
barEl.style.width = currentPct.toFixed(2) + '%';
barEl.setAttribute('aria-valuenow', Math.round(currentPct));
if (currentPct < targetPct) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
// ── rIC for non-urgent background tasks ──────────────────────────────────────
// Prefetch next page of results during idle time
function prefetchNextPage(page) {
requestIdleCallback(() => {
fetch(`/wp-json/myplugin/v1/inventory?page=${ page }`, {
headers: { 'X-WP-Nonce': wpApiSettings.nonce },
})
.then(r => r.json())
.then(data => {
sessionStorage.setItem(`inventory_page_${ page }`, JSON.stringify(data));
});
}, { timeout: 3000 });
}
// Auto-save form state to localStorage during idle time
let pendingSave = null;
document.querySelector('#my-admin-form').addEventListener('input', () => {
if (pendingSave) return; // already scheduled
pendingSave = requestIdleCallback(() => {
const formData = new FormData(document.querySelector('#my-admin-form'));
localStorage.setItem('admin_form_draft', JSON.stringify(Object.fromEntries(formData)));
pendingSave = null;
});
});
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, c =>
({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])
);
}
NOTE: requestIdleCallback is not available in Safari as of March 2024 — it is supported in Chrome 47+, Firefox 55+, and Edge 79+, but Apple has not implemented it in WebKit. For WordPress admin use (where browser choice is controlled by organisation policy), this is often acceptable — but for front-end theme code, a window.requestIdleCallback || (cb => setTimeout(cb, 0)) polyfill provides a graceful fallback: setTimeout(cb, 0) fires in the next event-loop tick, which is not truly idle-aware but avoids blocking the current synchronous execution. Also, both rAF and rIC callbacks are throttled in background tabs — rAF callbacks run at 1fps when the tab is hidden, and rIC callbacks may be deferred indefinitely. If your WordPress admin task must complete reliably regardless of tab visibility (e.g., an import that the user starts and then navigates away from), use a Web Worker for the heavy computation and communicate progress back to the main thread via postMessage instead.