Escaping the script injected script cargo cult
I've been migrating some of my sites' analytics over to Matomo Tag Manger and they provide a script injected script to add to the page source. In 2019 this is mostly a cargo cult pattern that has a better alternative that you probably should be using instead.
The snippet Matomo provides is fairly standard for this pattern and is in this form:
<script>
(function() {
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src='https://matomo.example.com/js/container_ABC123.js'; s.parentNode.insertBefore(g,s);
}());
</script>
There's a bit of obfuscation here but what it's doing is generating a <script src="…" async defer/>
and injecting it into the page. This <script/>
injected <script src="…" async defer/>
was the only way to ensure older browsers (IE<10) didn't render block on a <script src="…"/>
. Since support for defer
is now essentially universal this script within a script dance is no longer required and I'm directly adding this to the page's markup:
<script src="https://matomo.example.com/js/container_ABC123.js" defer></script>
There's an added bonus in that an inline <script/>
that comes after CSS will block CSSOM generation so a direct <script src="…" defer/>
is also removing this as another thing to think about.
The end result is the same (or even marginally better) and it's shifted complexity down the stack: the browser's runtime is now dealing with it for me.
It's also a nice example of the kind of progressive enhancement built into the HTML layer of the stack:
- In browsers that don't support
defer
it works. - In browsers that do support
defer
then it works better.
Script injected script soup
The standard snippet for injecting Google's Tag Manager is a real soup of minified JavaScript:
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-ABC123');</script>
It takes a bit of unpicking but this can be resolved into two <script/>
, one inline (which should go above any CSS) to initialise the data later:
<script>
(function(w,l) {
w[l] = w[l] || [];
w[l].push({
'gtm.start': new Date().getTime(),
'event':'gtm.js'
});
})(window, 'dataLayer')
</script>
And one external to load the tag manager:
<script src="https://www.googletagmanager.com/gtm.js?id=GTM-ABC123" defer></script>
This does simplify things slightly, you'll need to do a bit more work if you're renaming the window.dataLayer
variable - but, then again, if you're doing that you should be able to figure that out.