Method 1: using MutationObserver
beforescriptexecute no longer works in Firefox, nor did it work in Chrome. Luckily, there's an alternative, using MutationObserver, which is quite widely supported. The general idea is to add a MutationObserver at the beginning of pageload, which will run a callback whenever a new node is added to the DOM. Inside the callback, check for the existence of the <script> tag you want to alter or remove. If it exists, you can tamper with it (such as change its textContent, or have its src point somewhere else). Only after the callback finishes will the newly added script tag run, so this is an effective way to intercept and change Javascript on a page. Here's a live snippet example:
<script>
// This is the userscript code, which runs at the beginning of pageload
// Say we wanted to prevent the loading of the jQuery script tag below:
new MutationObserver((_, observer) => {
const jqueryScriptTag = document.querySelector('script[src*="jquery"]');
if (jqueryScriptTag) {
console.log('Found jQuery script tag; now removing it!');
jqueryScriptTag.remove();
// We've done what we needed to do, no need for the MutationObserver anymore:
observer.disconnect();
}
})
.observe(document.documentElement, { childList: true, subtree: true });
</script>
<div>Example content</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
console.log('After jQuery script tag. If done right, $ should be undefined:');
console.log(typeof $);
</script>
Here's an example userscript that blocks jQuery from loading in the <head> here on Stack Overflow:
// ==UserScript==
// @name Example jQuery removal
// @include https://stackoverflow.com*
// @run-at document-start
// @grant none
// ==/UserScript==
if (document.head) {
throw new Error('Head already exists - make sure to enable instant script injection');
}
new MutationObserver((_, observer) => {
const jqueryScriptTag = document.querySelector('script[src*="jquery"]');
if (jqueryScriptTag) {
jqueryScriptTag.remove();
observer.disconnect();
}
})
.observe(document.documentElement, { childList: true, subtree: true });
If you install this, you'll see that the loading of jQuery fails, resulting in lots of errors generated by Stack Overflow's JS.
Make sure to attach the MutationObserver as soon as possible - you need @run-at document-start to attach it before the page loads anything inside the <head>. (If using Tampermonkey / Chrome, you may need to enable experimental instant script injection to achieve this reliably - go to Tampermonkey Settings, Config mode: Advanced, scroll to the bottom, set Experimental Inject Mode to Instant.)
If you're writing userscripts for others, any you're using this technique, make sure to include instructions for instant script injection, since injection is not instant by default on Chrome.
Note that the observer is attached using
.observe(document.documentElement, { childList: true, subtree: true });
This attaches the observer to the <html> element, watches for added and removed immediate children with childList: true, and watches for added and removed nodes anywhere inside its descendants with subtree: true. Such a recursive listener is useful, but it's also computationally expensive on large dynamic pages, so make sure to remove it once it's achieved its purpose.
On a huge page, calling querySelector on every mutation could be costly, so you might want to iterate through the mutations (the first parameter to the observer's callback) and the mutations' addedNodes instead:
<script>
// This is the userscript code, which runs at the beginning of pageload
// Say we wanted to prevent the loading of the jQuery script tag below:
new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === 1 && addedNode.matches('script[src*="jquery"]')) {
console.log('Found jQuery script tag; now removing it!');
addedNode.remove();
// We've done what we needed to do, no need for the MutationObserver anymore:
observer.disconnect();
return;
}
}
}
})
.observe(document.documentElement, { childList: true, subtree: true });
</script>
<div>Example content</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
console.log('After jQuery script tag. If done right, $ should be undefined:');
console.log(typeof $);
</script>
You can also tweak inline scripts by assigning to their textContent inside the observer callback. The following snippet shows how you can change a random number generator function to always return 10, rather than 1-6:
<script>
// This is the userscript code, which runs at the beginning of pageload
new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.textContent.includes('const randomNum')) {
addedNode.textContent = addedNode.textContent.replace(
/const randomNum.*/,
'const randomNum = 10;'
);
observer.disconnect();
return;
}
}
}
})
.observe(document.documentElement, { childList: true, subtree: true });
</script>
<button>Roll Die</button>
<div>Results: <span></span></div>
<script>
const [button, span] = ['button', 'span'].map(tag => document.querySelector(tag));
button.onclick = () => {
const randomNum = Math.ceil(Math.random() * 6);
span.textContent = randomNum;
};
</script>
Method 2: using DevTools Local Overrides
Method 1 is the technique you can use when writing userscripts for others. But if the userscript is just for yourself, there's an easier method that can work in some situations. See this answer to the question Chrome Dev Tools - Modify javascript and reload: by going to the Sources -> Overrides tab in Chrome Devtools, and enabling local overrides, you can tell the browser to load a local copy of a .js instead of downloading the site's version. Then you can edit the local copy as desired, and it'll run instead of the built-in script. This is especially useful when tweaking large Javascript files - it's much more manageable than the MutationObserver approach.
But, there are some downsides:
- To modify inline
<script>// code here</script> tags, you'll have to download a local copy of the page. (So if the page serves dynamic content through the HTML response, you'll either have to re-save and re-tweak the .html for your overrides to use the new content, or you'll have to go back to the MutationObserver method.)
- This is a technique developers may be able to understand (after some experimenting), but it's not so user-friendly. It may not be the sort of thing you'd want to walk a casual userscript consumer through.