- Published on
How to measure page load time of SPA using GTM and GA4
- Authors
- Name
- Sabarni Das
Summary
We are trying to measure the time taken by each route in an SPA to load with all data and send the measurement to GTM. Which will be sent to GA4 from there. Along with the page load time we will log the time taken by each network request in the page (xhr/fetch or other static resources). Eventually we will be able to generate a report to see the performance of each route and further debug which resource under the route took the most time.
What is page load time?
Page load time is the time taken to load a complete page starting from entering the URL in browser.
Page load time for server rendered non SPA apps.
To measure the page load time if your app is completely server rendered and the page load is not dependent on any API call. check the following example.
We are using the performance API to calculate the load time. Considering the time between loadEventStart and navigationStart. Refer to the image from MDN below. Link to the doc

We are measuring till loadEvent start which means we are excluding the time taken by the onLoad handler.
This API does not take into account the time taken by any API(fetch/xhr) call.
As explained by this example, the fetch call takes at least 3 seconds to load but the load event does not wait for it.
Page load time for single page application
Let's see example of a SPA with multiple routes which load data from API response.
In this app there are three routes Home, Users and Products. Since the routing is handled client side (by react-router), document.addEventListener('load')
will only trigger once during the page load and not on route change.
Detect page load start on SPA route change
We can add an event listener on route change to start our measurement.
var oldUrl = window.location.href
navigation.addEventListener('navigate', (event) => {
var newUrl = event.destination.url
if (oldUrl !== newUrl) {
oldUrl = newUrl
init()
}
})
Detect page load complete
LCP
Using Web Vitals we can detect LCP of a page
<script type="module">
import {onLCP} from 'https://unpkg.com/web-vitals@4?module'; onLCP(console.log);
</script>
This is only triggered once per page load and the support for client side route (or soft routes) is still experimental (you have to turn on a flag on chrome and use a different branch from the repo). Which won't work in our case.
Polling
We can use the new Performance API to get the time for all resource load.
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(entry)
})
})
observer.observe({ type: 'resource', buffered: true })
Buffered flag here listens to past performance entries.
You can refer to the MDN docs for all the properties of entry. We will only need PerformanceEntry.duration
and PerformanceEntry.name
. Duration will give the us the time taken to load the resource and name will give use the URL. The entries can be filtered by PerformanceEntry.initiatorType
to get a specific kind of resource eg stylesheet or xhr.
The problem here is there is no web API to detect a request start, we will only be notified on request end. Because of this it is not possible to tell if there is any other request in progress. We can use a custom wrapper for fetch and xhr call but that won't be a generalized solution.
We can try to overcome this by polling. After every request completion event we will wait for certain amount of time if no other request comes we will consider all requests are resolved and if we get another request before the we time we reset the wait. This is not a foolproof solution if some resource takes more than the wait after other requests are completed then that won't get counted.
Below is the implementation.
;(function () {
var startTime = performance.now()
var timer
var logSent = false
var pollInterval = 3000
navigation.addEventListener('navigate', () => {
startTime = performance.now()
var logSent = false
if (timer) {
clearTimeout(timer)
}
})
function sendLog() {
timer = null
if (!logSent) {
console.log('Load time', performance.now() - startTime - pollInterval)
}
logSent = true
}
const observer = new PerformanceObserver((items) => {
items
.getEntries()
.filter(
({ initiatorType }) => initiatorType === 'fetch' || initiatorType === 'xmlhttprequest'
)
.forEach((entry) => {
console.log('network request', entry.name, entry.duration)
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(sendLog, pollInterval)
})
})
observer.observe({
entryTypes: ['resource'],
buffered: true,
})
})()
Let's try this with our SPA App.
Logging it in GA
Step 1: Define Your Custom Measure in Google Analytics
Before sending custom data from GTM, you need to set up a custom dimension or metric in Google Analytics to receive the data.
For Google Analytics 4 (GA4):
Access the Admin Panel:
- Log in to your Google Analytics account.
- Click on the
Admin
gear icon at the bottom-left corner.
Navigate to Custom Definitions:
- In the
Data display
section, select Custom definitions. - Click on tab Custom metrics.
- Then click on Create custom metric.
- In the
Create a New Custom Metric:
- Name: Page Load Time.
- Scope: Event.
- Description: Optionally, provide a description.
- Measurement Unit: Milliseconds.
- Event Parameter: pageLoad.
- Click Save.
Create another Custom Metric:
- Name: API Load Time.
- Scope: Event.
- Description: Optionally, provide a description.
- Measurement Unit: Milliseconds.
- Event Parameter: apiLoadTime.
- Click Save.
Step 2: Add the script to your page
2.1 Make sure GTM script is added to your page
it should look like this:
// Google Tag Manager.
window.dataLayer = window['dataLayer'] || [];
document.addEventListener('DOMContentLoaded', () => {
/** init gtm after 3500 milliseconds - this could be adjusted */
setTimeout(initGTM, 3500);
});
function initGTM() {
if (window.gtmDidInit) {
return false;
}
// Flag to ensure script does not get added to DOM more than once.
window.gtmDidInit = true;
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
// Ensure PageViews is always tracked (on script load)
script.onload = () => {
dataLayer.push({
event: 'gtm.js',
'gtm.start': new Date().getTime(),
'gtm.uniqueEventId': 0
});
};
script.src = 'https://www.googletagmanager.com/gtm.js?id=YOUR ID';
document.head.appendChild(script);
}
2.1 Make sure GTM script is added to your page
;(function () {
try {
window.dataLayer = window.dataLayer || []
var startTime = performance.now()
var timer
var logSent = false
var pollInterval = 3000
navigation.addEventListener('navigate', () => {
startTime = performance.now()
var logSent = false
if (timer) {
clearTimeout(timer)
}
})
function sendLog() {
timer = null
if (!logSent) {
window.dataLayer.push({
event: 'pageLoad',
pageLoad: performance.now() - startTime - pollInterval,
})
}
logSent = true
}
const observer = new PerformanceObserver((items) => {
items
.getEntries()
.filter(
({ initiatorType }) => initiatorType === 'fetch' || initiatorType === 'xmlhttprequest'
)
.forEach((entry) => {
window.dataLayer.push({
event: 'apiTimings',
apiTimings: {
url: entry.name.replace(location.origin, ''),
value: entry.duration,
},
})
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(sendLog, pollInterval)
})
})
observer.observe({
entryTypes: ['resource'],
buffered: true,
})
} catch (error) {
console.error('Error:', error)
}
})()
Step 3: Configure Google Tag Manager
3.1 Create a Data Layer Variable
In GTM, go to Variables:
- Navigate to Variables in the left sidebar.
- Under User-Defined Variables, click New.
Set Up the Variable:
- Variable Type: Select Data Layer Variable.
- Data Layer Variable Name: pageLoad.
- Click Save.
similarly create apiUrl and apiTimings
3.2 Create a Trigger for the Custom Event
Go to Triggers:
- Click on Triggers in the left sidebar.
- Click New.
Set Up the Trigger:
- Trigger Type: Select Custom Event.
- Event Name:
pageLoad
. - Click Save.
similarly create one for apiTimings
3.3 Create or Modify a GA4 Event Tag
Go to Tags:
- Click on Tags in the left sidebar.
- Click New.
Set Up the Tag:
- Tag Type: Select Google Analytics: GA4 Event.
- Configuration Tag: Choose your existing GA4 Configuration tag. If you don't have one, create it with your GA4 Measurement ID.
- Event Name: pageLoad.
Add Event Parameters:
- Under Event Parameters, click Add Row.
- Parameter Name:
pageLoad
. - Value: Click the variable icon and select the Data Layer Variable Data Layer Variable - pageLoad.
similarly create one for API timings measurement
Finally publish the changes. Now, every time a user loads your blog page, the Google Analytics event will be sent with the pageLoad
parameter indicating that the page has been loaded.