How to remove a script tag reliably before it is loaded?

Viewed 444

I've created a mutation observer to remove (block) scripts under certain conditions.

There are several SO solutions which suggest it can work. Eg: https://stackoverflow.com/a/65453574/4688612

But it doesn't work in my case for <script src="https://www.googletagmanager.com/gtag/js?id=UA-12345678-9"></script>

For some reason the network call to googletagmanager.com still happens on every page load.

Google Chrome and Firefox both load the script.

Now I'm not sure if using the mutation observer method is simply not reliable or buggy, or if it is a bug in my code.

Is there a reliable solution for this?

Here's my code.

<!DOCTYPE html>
<html lang="en">
    
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    
    <link rel="shortcut icon" href="#">
    
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"
            integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous">
            </script>
    
    <script>
    
        const observer = new MutationObserver( (mutations) =>{
                mutations.forEach(({addedNodes}) => {
                    [...addedNodes]
                        .forEach(node => {
                            $(node).remove()
                        });
                });
            })

        observer.observe(document.head, { childList: true });
   
    </script>
    
    <script src="https://www.googletagmanager.com/gtag/js?id=UA-12345678-9"></script>
        
</head>
    
<body></body>    
</html>
    ```
2 Answers

The script should be added instead

As discussed in the comments, it is unreliable to remove the script with an observer.

  • The browser can prefetch the file as soon as the src URL is known, independently of the HTML tag being rendered
  • The browser does not have to be honest or perfect. It may prefetch the file without listing it (yet) in the developer tools.
  • Not all versions of all browsers implement mutation observers: see caniuse MutationObserver. This may attract people using older browsers on purpose and complaining. So not a reliable way for legal requirements.

Instead the script should be added when needed:

(function(p,a,n,t,s){
  t=p.createElement(a),s=p.getElementsByTagName(a)[0];
  t.async=1;t.src=n;s.parentNode.insertBefore(t,s)
})(document,'script','https://www.googletagmanager.com/gtag/js?id=UA-12345678-9');

Testing your observer locally

The following will illustrate what may be the issue here.

I have created an index.html just as yours but with a locally loaded script test.js. I also added console.log(node); in your observer.

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MutationObserver test</title>

    <link rel="shortcut icon" href="#">

    <script src="https://code.jquery.com/jquery-3.6.0.min.js"
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>

    <script>

        const observer = new MutationObserver( (mutations) =>{
            mutations.forEach(({addedNodes}) => {
                [...addedNodes]
                    .forEach(node => {
                        console.log(node);
                        $(node).remove()
                    });
            });
        });

        observer.observe(document.head, { childList: true });

    </script>

    <script src="test.js"></script>


</head>

<body></body>

</html>

test.js

console.log("I got loaded before you could remove me.");

This results in the following console log:

index.html:21 " "
index.html:21 <script src=​"test.js">​</script>​
index.html:21 " "

So the observer is working, as it also catches empty nodes with just whitespace characters.

However in the Network tab the test.js is still listed as loaded:

Name        Status      Type        Initiator   Size        Time
index.html  Finished    document    Other       972 B       1 ms    
test.js     Finished    script      index.html  56 B        3 ms    
jquery-3... 200         script      index.html  31.0 kB     10 ms   
index.html  Finished    text/html   Other       972 B       1 ms    

It seems as though the HTML node was removed, the browser is prefetching the code. This likely happens because the HTML document itself will transfer regardless. Then the browser parses the HTML and sees all files to be requested. As soon as the HTML document is turned into a DOM the browser runs the observer, removes the node and just in time will not execute the JavaScript from the nodes after the observer.

See this counterexample where I comment out the observing part:

// observer.observe(document.head, { childList: true });

In this case the console output will read:

test.js:1 I got loaded before you could remove me.

The network panel will look nothing different. So it seems as I'm using Chrome, it tries to perfect speed and will load and cache the JavaScript but it will not execute it since its node got removed.

Possible TagManager / Google Anyltics insights

If you have access to alter the TagManager's tags you might just add <script>console.log("TagManager code executed");</script> and confirm this way code execution is correctly suppressed for the removed tag. So confirming the same as my example with a local JS file but in TagManager directly.
Another way is Google TagManager's excellent Debug mode.

If Google Analytics is used here as well you might see your requests (or lack thereof) in the GA Real-Time view (well... bad documentation with no screenshots. Just look for real-time in Analytics you will find it).

Try with following solution

Add defer or async to google tag manager script tag. this make google tag manager script after page load.

<script src="https://www.googletagmanager.com/gtag/js?id=UA-12345678-9"></script>

Changed to

<script defer src="https://www.googletagmanager.com/gtag/js?id=UA-12345678-9"></script>

or

<script async src="https://www.googletagmanager.com/gtag/js?id=UA-12345678-9"></script>

Also call your removing script for the google tag manager code, this will make sure you script will load before google tag manager script load.

Learn more about deferand async Click Here

Following code is working fine for me

    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>

        <link rel="shortcut icon" href="#">

        <script src="https://code.jquery.com/jquery-3.6.0.min.js"
            integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous">
            </script>

        <script>

            const observer = new MutationObserver( (mutations) =>{
                    mutations.forEach(({addedNodes}) => {
                        [...addedNodes]
                            .forEach(node => {
                                $(node).remove()
                            });
                    });
                })

            observer.observe(document.head, { childList: true });

        </script>

        <script defer src="https://www.googletagmanager.com/gtag/js?id=UA-12345678-9"></script>


    </head>

    <body></body>

    </html>
Related