Escaping the script injected script cargo cult

Orde Saunders' avatarPublished: by Orde Saunders

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.