How Do Userscripts Handle Dynamic Content in AJAX or React?
Userscripts often execute before a webpage finishes rendering or
before dynamic content is fetched asynchronously via AJAX or framework
engines like React. Because modern web applications continuously inject,
update, and remove DOM elements after the initial page load, standard
userscripts fail if they rely on simple single-run selectors. To
reliably interact with dynamically loaded elements, userscripts must
employ strategies such as dynamic polling, native browser APIs like
MutationObserver, standard event listeners, or network
interception techniques to detect and manipulate content as it
appears.
The Challenge of Dynamic Content
Traditional web pages loaded all HTML, CSS, and dynamic content in a
single synchronous HTTP request. When the DOMContentLoaded
event fired, userscripts could safely assume all elements were present
in the DOM.
Modern single-page applications (SPAs) built with React, Vue, or Angular work differently:
- Asynchronous Fetching: Content is retrieved via
fetch()orXMLHttpRequest(XHR) long after the page has loaded. - Client-Side Rendering: Frameworks construct and insert DOM elements dynamically based on state changes.
- DOM Re-renders: React regularly unmounts and remounts components, wiping out any direct DOM modifications or event listeners a userscript previously attached.
Primary Approaches to Handle Dynamic Content
1. Observing the DOM
with MutationObserver
The most efficient and modern browser-native solution is the
MutationObserver API. Instead of constantly checking the
DOM on a timer, MutationObserver listens for specific
structural changes within the document tree and fires a callback
whenever nodes are added, removed, or modified.
// Function to handle target element once detected
function handleElement(node) {
node.style.backgroundColor = 'yellow';
}
// Set up the observer
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
// Ensure it's an Element node
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('.dynamic-target-class')) {
handleElement(node);
}
// Search children of added nodes
const targets = node.querySelectorAll('.dynamic-target-class');
targets.forEach(handleElement);
}
});
}
}
});
// Start observing the target container or entire document
observer.observe(document.body, {
childList: true,
subtree: true
});- Pros: Highly performant, event-driven, minimal CPU overhead.
- Cons: Can generate a high volume of events on heavy React re-renders if not scoped carefully.
2. Polling
with Utility Libraries (waitForKeyElements)
Polling involves checking the DOM at fixed intervals until a specific
element exists. While raw setInterval() calls can be
inefficient, established helper functions like
waitForKeyElements streamline this process and handle
cleanup automatically.
// Utility function for waiting on dynamically loaded elements
function waitForKeyElements(selector, callback, waitOnce = true) {
const elements = document.querySelectorAll(selector);
elements.forEach((element) => {
if (!element.dataset.userscriptProcessed) {
element.dataset.userscriptProcessed = 'true';
callback(element);
}
});
if (!waitOnce || elements.length === 0) {
setTimeout(() => waitForKeyElements(selector, callback, waitOnce), 300);
}
}
// Usage
waitForKeyElements('.react-loaded-button', (btn) => {
btn.addEventListener('click', () => alert('Clicked!'));
});- Pros: Simple to implement, works reliably across legacy and modern sites.
- Cons: Uses background CPU timers; slightly less
reactive than
MutationObserver.
3. Intercepting AJAX / Network Requests
Rather than waiting for React or AJAX to update the DOM, userscripts
can hook into the underlying network calls by overriding
window.fetch or XMLHttpRequest. This allows
the script to read or modify data before the web application renders it
to the screen.
// Intercepting native fetch requests
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const response = await originalFetch.apply(this, args);
// Clone response to inspect data without consuming the stream
const clone = response.clone();
if (args[0].includes('/api/v1/dynamic-data')) {
clone.json().then(data => {
console.log('Intercepted AJAX response:', data);
// Trigger userscript logic based on payload data
});
}
return response;
};- Pros: Allows reaction to data updates instantly, independent of UI changes.
- Cons: Requires
@run-at document-startand grant settings (likeunsafeWindow) depending on the userscript manager engine (Tampermonkey, Violentmonkey).
4. Leveraging Event Delegation
For simple interactions (such as handling clicks on dynamically
created buttons), event delegation avoids the need to watch for element
creation entirely. By attaching a single event listener to a permanent
parent container (like document.body), events trigger via
event bubbling regardless of when the child target was added.
document.body.addEventListener('click', (event) => {
const target = event.target.closest('.dynamic-button');
if (target) {
console.log('Dynamic button clicked:', target);
}
});Best Practices Summary
| Strategy | Best Used For | Performance Impact |
|---|---|---|
MutationObserver |
Detecting elements dynamically mounted by React/Vue | Low (Event-driven) |
Polling (setTimeout) |
Simple scripts or fallback detection | Medium (Depends on interval) |
| Network Interception | Reading/modifying data before rendering | Very Low |
| Event Delegation | Handling user clicks/inputs on dynamic nodes | Negligible |