A Faster Google Tag Manager
I'm going to go out on a limb here and say that nobody really loves Google Tag Manager. But it is undeniably useful.
The Problem with Google Tag Manager
Google Tag Manager works by allowing you to define tags in a web UI, which are then inserted into your code via a small snippet. The concept is simple enough and it helps grease the wheels between marketing and engineerings. But the problem comes when you run a Google PageSpeed Insights test.
Almost immediately, you get feedback that your website’s performance has taken a hit. So here’s the frustrating part: on one hand, Google tells you to install Tag Manager to use Google Analytics and Ads. On the other hand, it tells you to uninstall it to speed up your site! It’s a catch-22. You want the flexibility of adding tags on the fly, but at what cost?
Why Google Tag Manager Slows You Down
The issue is that once you add Google Tag Manager to your site, every page has to make a request to determine which tags to load. The page loads, requests the tags, and only then can it pull in additional scripts as needed.
That's:
- DNS lookup
- TCP connection
- Waiting for the response
- Content Download
- JS Parse
- JS Execute
before you even know what JavaScripts you want to start loading.
Examples
Hardcoded <script> in <head>: 150ms
Let's use just a single tag to keep things simple. The easiest way to add a tag is to just paste it into the head of your page. We'll use Posthog as our example tag and add it to a dirt simple Rails "hello world" page.
Result:
- Page load event in 105ms.
- Posthog ready around 150ms.
- Marketing has no visibility and doesn't love it because they have to make a ticket to add a tag.
<head>
...
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_apikey',{api_host:'https://us.i.posthog.com',
})
</script>
</head>
Google Tag Manager: 285ms (90% slower than the original)
To switch over to Google Tag Manager, your code will change to this.
<head>
...
<!-- Google Tag Manager -->
<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-ABCDEFG');</script>
<!-- End Google Tag Manager -->
</head>
And of course you can see the problem already, the code no longer knows about the Posthog script. It won't know until the GTM script loads & runs.
Result:
- Page load event in 210ms.
- Posthog ready around 285ms. 90% slower than the original.
Yikes! A 100ms+ delay just from adding Google Tag Manager. According to GPT anyway in 2024, Google Tag Manager (GTM) is used by approximately 48.9% of all websites. That’s a lot of time spent waiting.
The ideal solution would be for your pages to somehow "know" which tags to load without that extra round trip. But for that to happen, your server-side frameworks would need to coordinate and decide on the correct tags for each page... hey wait a minute...
Solution: Dynamic Configuration: 110ms (As fast as the original)
And that’s exactly where dynamic configuration comes to the rescue.
Dynamic configuration allows your pages to pre-render with the correct tags, without making additional requests post-load. This eliminates the performance hit that comes from waiting for tags to load.
<head>
...
<%= raw(Prefab.get("marketing.header.scripts")) %>
</head>
Result:
- Page load event in 80ms.
- Posthog ready around 110ms. (This particular load was faster than putting it in the head directly, but that's random. It will just be "the same")
In this example it was literally twice as fast as GTM, saving a solid 100ms.
What about the rest of Google Tag Manager?
Are we giving anything up by not using Google Tag Manager? Audit trail, targeting, testing, etc?
No! In fact, I'd argue that you're going to be happier with dynamic configuration. Why?
- Oversight: With a Slack integration, you can track who’s making changes to your script tags, preventing the scenario where marketing adds extra tracking pixels without you knowing.
- Audit Logging: You still get the audit logs that Google Tag Manager offers, but now they’re integrated with the rest of your dynamic configuration.
- Access Control: You can control who has access to make changes to your dynamic configurations with fine grained ABAC.
- Unified targeting: You retain all the capabilites of Google Tag Manager's targeting. Want some scripts to load only on internal pages? No problem. Want to only load the conversion tracking script on your checkout page? No problem. #its-just-config so you have good choices about whether to make these separate configs or one big one. One example of how we could do this would be like so:
In short, dynamic configuration not only solves the performance problem but also improves transparency and oversight.
If you're interested in saving 100ms on page speed while retaining the flexibility of Google Tag Manager consider using a dynamic configuration tool like Prefab.
Note: Applications Without Server Side Rendering
Note: If your applications is not server side rendered, you can still use dynamic configuration as a replacement for Google Tag Manager, but the performance win will be much smaller. Prefab client libraries will make a request and pull back both you feature flags and your configuration and then you can use the same eval to load the scripts. The win here vs GTM would be that you're using a single tool, not a feature flag tool and and tag manager, but we will still need to make a request to get the scripts. You may still appreciate the Slack integration and audit logs of Prefab as a way to manage your tags.