 Hi, I'm Tony Conway and I'm a web ecosystem consultant at Google. If you're a web developer, you've probably heard of Core Web Vitals or CWVs. These metrics are a way to quantify user experience on your site based on real user interactions. There are three Core Web Vitals that Google Chrome measures on a regular basis. Largest Contentful Paint, or LCP. First Input Delay, or FID. And Cumulative Layout Shift, or CLS. Each of these metrics reflects a different part of the user experience. LCP measures how quickly important elements render on the page. FID measures how quickly your site can handle a user's first input. And CLS measures visual stability, incrementing when elements move around in the viewport. If you want to understand more about how these metrics are calculated and what constitutes a good score, check out web.dev slash vitals. Now the publicly available Chrome user experience report dataset is a great resource for tracking your site's CWVs. However, it's only updated once a month and only includes data from users who have opted into Chrome's performance monitoring. It also includes limited data about which pages were measured and which elements on the page contributed to your scores. If you're serious about improving performance for your users, implementing a real user monitoring workflow is a must. If you want to get a more granular view of how your site is performing, you can implement WebVitals.js. In this workshop, I'll show you how to import WebVitals.js, what data is available to you, how to send CWV measurements to a Google Analytics property, and how to query and visualize that data using BigQuery and Data Studio. Before you start, you'll need a Google Analytics account with a GA4 property, a Google Cloud project with BigQuery enabled, a Chromium based browser, a text editor of your choice, somewhere to host test pages like a local server or a GitHub pages deployment, and access to a production site where you can deploy WebVitals.js for real users. If you want to copy and paste code samples, you can find them in the code lab linked in the description below. Once you've got those things lined up, you're ready to go. So let's get into it. Okay, first things first, we want to link our Google Analytics 4 property to our BigQuery project so that we can start analyzing as soon as we move our WebVitals code to production. Go to analytics.google.com and select the GA4 property you want to use for reporting WebVitals. I'm going to be using this WebVitals.js demo property I've already created. We'll navigate to admin, then click on BigQuery links menu item in the property settings. Click the link button, then click choose BigQuery project. You'll see a list of projects you have access to. You can use the search bar to filter for your Google Cloud project. Now my project is called web-vitals-ga4-demo. Check the box next to the project and click confirm. If you don't already have a data location set for your project, select one. I'm using Europe West 1 here and then click next. If you want to exclude GA4 streams or events from the BigQuery export, click on configure data streams and events. Deselect any streams you want to exclude and click add to select events that you want to exclude by name. I've used this account before, so my WebVitals events are already showing up. Select a frequency for your export. Daily frequency will give you a day partition table that you can use for historical trend analysis and streaming frequency will give you data from the last day up to the last few minutes. I'm going to select both and then click next. Review the settings and click submit to set up the BigQuery link. Link created. Great. Now that's set up, we can start experimenting with the WebVitals.js library. All the code snippets I'm about to show you can be found in the code lab linked below, which we can see here. And you can clone the WebVitals code lab repository on GitHub if you want to jump ahead. Now I've got a very basic web server set up in VS Code, which uses Express.js to deliver files directly from this static folder. So let's start by creating a basic page which prints the output of the WebVitals library functions to the console. I'm creating a file called basic.html in the static folder. And I'm going to copy in the HTML from the second page of the code lab. Copy and paste. And I'll save that. Now we need to add the WebVitals library as a module. All browsers that fully support WebVitals.js also support module imports. And browsers that don't support modules will just skip to this script tag. So we'll copy and paste in script tag with type equals module and import the getcls, getfid, and getlcp functions. We're using a hosted version of the WebVitals library to save time here, but you can always self-host the library if you want to. Take a look at the WebVitals.js GitHub repository for more information on import methods. Now that the WebVitals functions are available, we can call them passing console log in as an argument. When the WebVitals functions are ready to fire, their outputs will be directly printed to the console and we can take a look to see what they contain. So we'll save that page again and start our web server to view the page. So let's open a new Chrome tab. We've got one right here. And we can open the developer tools. Now we're loading locally, which will typically happen very quickly. So let's use the dev tools to simulate a slower response. We'll go to the network tab and we'll select slow 3G in the throttling menu. We'll also keep the cache disabled for now. When we load the page, our slow connection will mean that the elements, especially this cat picture, load very slowly. Let's refresh and take a look at the console. As I mentioned, the cat image loaded very slowly. So if we click on the page, that will end measurement of LCP and FID and there we go. Both events are logged by WebVitals.js to the console. If we select a different tab and then come back, we can see that CLS measurement has been triggered as well. CLS events typically fire when the CLS value has changed and the user navigates away from the page, refreshes the page or the page visibility changes to hit for some reason. You can also force CLS to be reported every time the value updates by adding a second argument of true to the get CLS function that would be right here. But we don't want to do that for now. So let's look at this output in more detail. Each of the functions returns an object with information about the corresponding metric. In the LCP object, we can see the value, which in this case is 13,748 milliseconds. That's a very slow time because we waited until the image finished loading over that slow 3G connection before interacting with the page. Each object has an ID property, which is unique to the event type and page view. Each object also has an entries property, which contains information about the CWV measurement. In the LCP object, entries contains an array of all the elements that were considered as LCP candidates. We can see that the first element in the array is the text paragraph, which was rendered relatively quickly. That was rendered in 2000 milliseconds. And the second element in the array is the cat image, which took much longer. And that was what was reported as the LCP. Both of these performance entries contain the area of the element in pixels, which is stored in the size property, and the time at which the element was rendered, which is the start time property. And if it's available, the URL and ID of the element will also be stored. The entire DOM object is accessible through the element property. So let's take a look at FID. If we expand the object, we can similarly see the value, which is 2.3 milliseconds. We can see the ID, and we can see another entries property. Now, the entries property for FID will always be an array of a single item, and it tells us what type of event the first input was. So in this case, it's pointed down. And the time in milliseconds between the user input, so that pointed down event, and the main thread becoming available, and that is the gap between start time and processing start, and that gap is 2.3 milliseconds. The CLS object also contains an entries property, an ID, a value, and, as with the others, a delta, which is more relevant with CLS. If the CLS function is triggered several times in the life cycle of the page, the value will increment by the amount recorded in the delta. For example, if we remove the element on this page using JS and then switch tabs, so we document getElementsByNameImage, the first image, remove, and then switch tabs, we can see the CLS function fired again, showing us an increased total CLS value. It's gone from 0.0069 to 0.0082, and the delta shows us how much it increased by 0.0012. WebVice with JS reports the same ID for each CLS event, which we can use to our advantage later on. We can also see for each CLS event, layout shift entries for that specific CLS update. We only had one element move in each of the layout shifts. That was the PTAG, which we can find in the layout shift attribution. We can see the previous size of the element and the new size and position of the element in previous rect and current rect. So now that we know what data is available to us in WebVice with JS events, we need to structure that data in a format that we can use to send to Google Analytics. We'll go back to VS Code and create a new file called diagnostics.html. We'll copy across the code from our basic page to get started. We need to add in the global tag for our Google Analytics property. We'll go back to Google Analytics admin and we'll click on Data Streams. If you don't really have a Web Data Stream setup, set one up for your domain. I'm using a made-up site here, tonyscoolwebsite.com. You can click on your stream and then copy the code from the global site-to-tag panel. So we'll copy that, copy, and then we're going to paste that into the head element of our diagnostics.html file. We'll tidy that code and save. So now the page is ready to start reporting GA events, but we still need to structure the information. So we still want to import these getCLS, getFID, and getLCP functions, but we don't want to report them to the console anymore, so we'll delete this code. Something that's really useful when you're trying to debug high CWV metrics is being able to attribute the metric value to a particular node on the page. So let's copy across this function from the code lab called getSelector. This returns a string representation of a given node's place in the DOM. It takes the node in question as an argument, collects whatever data it can about it, such as its classes, ID, or tag name of the node, and then iterates through its parent nodes. If it can't get any of that information, it simply returns this empty string. Next, we'll collect some information about layout shifts. When getCLS fires, it tells us about all the layout shifts that have happened on the page, but that could be dozens of shifts, which will be very hard to analyze. Instead, let's use these two functions to get the largest layout shift entry and the largest layout shift source within that entry. This will let us focus on only the worst CLS offenders on our page. As you optimize to eliminate the worst offending layout shifts, new worst offenders will appear in your reports, and you can focus on them instead. So let's copy and paste. And what's next? Let's look at FID. There are a number of things that we could look at for FID, and it will depend on how your page is built. For this example, we're going to look at whether the first input from the user happened before or after the DOM content loaded event. We can look at this to determine if users are trying to interact with the page before our synchronous resources have finished loading. So we'll copy that and paste. We also want to know which element the user tried to interact with. So we can use this function, get FID debug target to do that. And lastly, on the subject of FID, we want to know what the event type was. So we will copy across this function, which does the same. I'll tidy that. The last step before we send the data off to Google Analytics is to add a function that structures the output of all the above functions appropriately, depending on which CWV metric is being reported. This getDebugInfo function takes in the name of the metric and its entries array, and returns an object with the appropriate properties, depending on what the name of the CWV is. So we'll copy this and paste it. You can see the functions that we copied earlier being used in each of these code blocks. Okay, great. We have all the code in place to send our CWV measurements to our GA4 property. We just need to copy across one more function called send to Google Analytics. When we call the web vitals functions, we will pull out the name, delta, value, id and entries from the event. We'll call gtag. Remember that we added the global tag in the head of the page with event as its first argument. The next argument is the name of the event, which is how we'll identify whether it was fcp, fid or cls. And finally, we'll pass this object through, which contains our values. We'll pass the delta from web vitals as the value, and then the id of the event as metric id. Value is a GA4 specific field, and if we pass multiple events with the same name and value property, GA4 will sum them. This means that we can pass multiple cls events and get the right final value in the GA4 UI. We've passed the id, the value and delta from web vitals using these custom metric id, metric value and metric delta fields, and we'll be using these later in BigQuery. Finally, we add whatever is returned from get debug info. This dot dot dot notation extracts the returned keys and values and sets them as keys and values in the object we're passing the GA4. All that's left to do is call the web vitals functions with send to Google Analytics as the argument. So we'll copy and paste that, and then we'll save our file. So we want to make sure our data fires correctly into Google Analytics. So we'll head back to Chrome. We don't want to be here all day, so I will turn off the throttling in the network tab. Just put that back on no throttling. And then if we navigate to diagnostics.html, we won't see our web vitals events in the console anymore. However, we can see network calls to Google Analytics sending our fcp, fid and cls values. We can see, once we've clicked, wait a moment, and we can see lcp and fid. And if we change tabs and wait a moment, we should see cls being reported as well. We can confirm that the data is coming through in Google Analytics by going to reports and then realtime. And we can see those events appearing straight away in the event count panel. We can click on any of the events to see which keys are coming through. We've got our metric ID, metric value, metric delta. And we can click on the key to see which values were reported. This is 0.069 cls. Amazing stuff. Now we're sending web vitals events straight into GA and seeing the data populate. Before we move on to the next section of this workshop, querying the data in BigQuery, I'd strongly suggest you add the web vitals code we've looked at to your production site. The following examples would be a lot more useful and make a lot more sense if you have real user data flowing into BigQuery. Once your code is live and you're ready to start querying, come back to this video or continue along with the code lab. They're not going anywhere. Welcome back. Brilliant. Now that you've got web vitals data coming in from the real world, it's time to start analyzing it. So we will open the Google Cloud Console and we'll navigate to the BigQuery workspace. If you don't have it pinned already in your navigation bar in this resources section, you can find it under the analytics header in this navigation menu. It's all the way down here somewhere. There is BigQuery. Now, unfortunately, because I've only just set up the link between Google Analytics and BigQuery, I have very little data to analyze. This is the data set you can see from previous experiments. I've only got two or three days of data. What we're going to do is change the project to demoverse. I'm going to use this demoverse project, which is data from the Google Merchandise Store, and we can take a look at what's available to us here instead. So you can see that the Google Merchandise team have multiple analytics properties. The GA4 property that is capturing this web vitals data is this one down here. So if we expand the data set, we can see two tables. There's the events table and the events underscore intraday underscore table. The events underscore table contains historical event data up to the date before last. I'm recording this on the 6th of April, so I can query events' date partitions up to and including the 4th of April. If we want to get more recent data, including yesterday and today, then I would need to query the events intraday table. So we'll take a look at the structure of the events table. There are columns for event date, event timestamp, and event name. An event name will be populated with the values we passed to GA4 from the web vitals library, LCP, FID, and CLS. The event params column will contain all of the data that we passed through for each event nested against a key. The value has keys for each of the data types that could be passed through. Now, the data set contains events that aren't related to CWVs and there might be multiple rows representing CLS for each page view. Rather than querying directly, we need to do some pre-work on our data before we can start analysing it. So what we're going to do is copy this sub-query from the code lab and paste it into the query editor. Copy and paste it into this query editor so that we have a more manageable temporary table called web vitals events to work with. Now, we need to update the table name so that we're querying on line 11 to use our project ID. So for this project, it's adh-demo-data-review and the data set ID, which is analytics underscore and then the ID of the analytics account. So that is 229787100. This sub-query has a few nested queries and can be a little bit hard to pass. The important thing is that it uses metric ID and metric value to select only the last received value per metric per page view. We've got two where conditions as well. The first one filters down to just CLS, FID and LCP events, leaving out things like page views and conversions. The second table uses the table suffix parameter in place of this asterisk to limit our query just to the last 28 days. So we can see between format date, date 28 days ago and date yesterday. You can see on the left that we have over 400 days of data for this account, so we don't want our query to run over all of that data every single time we execute it. So now that we've got a temporary table in place which only contains CWV events from the last 28 days, what do we do next? Why don't we take a look at CLS and find the worst performing pages on our site using this example from the code lab. First, we're going to use this subquery on our temporary table to pull out the path of the page where the event was recorded, the debug target, and you'll remember for CLS that was the selector for the element which moved in the viewport causing the largest CLS value change. And lastly, we're going to take out the metric value, which was the final reported value of CLS for the given page view. We're using this coalesce function to pick either a double value or an int value depending on how GA has stored it. This will make reusing parts of this query easier later on. We're using this where condition to filter down to just CLS events. And with that subquery completed, our main select statement is going to show us the page path, the debug target. We're going to select the 75th percentile of CLS scores for the page path and debug data pairs and then the number of page views that we use to calculate that score. Lastly, it is sensible to filter your results to only show rows with a minimum number of page views. Otherwise, you might have to spend time wading through a lot of statistical outliers with bad scores but only one or two page views. We use this having page views greater than 50 to focus on issues that are occurring more frequently. You can tune this number based on your site traffic. So let's click run and see what results we get. Okay. So I can see here that there seem to be some repeating issues. Firstly, the main HTML node of the page is causing substantial layout shifts on Android mobile and possibly on desktop. This could be caused by incorrectly implemented animations or render blocking code that is delivered too late. I can also see that there's a recurrent problem with a footer element so that would also be worth investigating. I'll work with the Google Merchandise Store developers to optimize these issues and over time we'll stop seeing them appear at the top of the results. Different pages and debug targets will start to appear which we can go on to optimize and eventually eliminate from the results. Now, we can tweak this query to monitor any of the core web vitals simply by changing this where condition. If we change it to LCP or FID we'll get the worst performing parts and elements for each of those metrics. We could also add the event date as metric date in the sub-query and group by the first three columns so that we can monitor changes over time. Now, from here we could just click explore data and click explore with Data Studio and analyze our results in a chart. But that would be a pretty inefficient way to work running the query and creating a new Data Studio dashboard every time we want to see how our site is performing. Alternatively, we could simply add our events and events intraday tables directly as resources in Data Studio. But that would also be inefficient potentially running very large expensive queries every time somebody tries to look at a dashboard. So what we want to do instead is materialize our data to a simpler, flatter table. This will allow us to process less data and load results faster so we don't have to query the entire data set every time we want to take a peek at what's going on. Now, if you want to materialize data on an ongoing basis you'll need the BigQuery Data Transfer API enabled in your project. Unfortunately, I can't enable this in the demo-verse project because I don't have the right privileges but I can read data from this project in my own WebVitals GA4 project. So let's switch over to that. If we're going to materialize data we need a data set. Let's go ahead and create a new data set in the project. We'll call it WebVitals Materialized. We have to use underscores here. And the data set needs to have the same region as the table I'm transferring from. So I'll set the region as US because that's where the demo-verse project data is hosted. So I'll click Create Data Set and here it is, my data set. Rather than creating a query from scratch we'll use this query from Part 9 of the codelab. The first thing we want to do after we've copied and pasted it the first thing we want to do is change the project and data set for our source table right here on Line 85. So we'll update this to ADH-Demo-Data-Review and then it's Analytics 229787100. The default for this query is to run over all of your analytics data set in the event table because of this asterisk. This is okay if you've only got a few days or weeks of events to backfill but the demo-verse data set has, as we saw before, over 400 days of data. So we can use the same table suffix syntax we used before to collect a limited number of days. We'll paste that as an extra condition here and just ask for data in the last 28 days until two days ago. Next up we need to change this create or replace statement right at the top of the query to point to the right destination table. So we will update the project ID to my web vitals GA4 demo project and the data set ID to the one I just created which is web underscore vitals underscore materialized. We can call the table whatever we want let's leave it as web vitals summary. So that is all ready to go and we can hit run and fairly quickly we should see our new table appear. The other thing we'll want to do is set this up to run on a regular basis. Now we've got two options we can either run this query on a daily basis overwriting our existing table or we can keep our historical events in place and append new events onto the table. We talked before about where our most up-to-date events are and we know that events intraday includes events from yesterday. So we're going to tweak our materialized query in a few ways. We'll save this backfill version of the query in case we need it later. I'll call it materialize web vitals backfill save that's save now and then we'll copy and paste the contents over to a new query for editing. Firstly we want to get rid of this create or replace table statement as we don't want to overwrite our table completely. Then we want to change our source table to events underscore intraday and finally we can set the table suffix condition to just look for yesterday's data. So we can change this between to an equals we can remove the end of the line and we can set the interval to one day in the past. If you haven't already enabled the data transfer API do so. Now we can click schedule to set up a daily job. Give the scheduled query a name we could call it materialize web vitals daily and set it to repeat daily. If you wanted to have even more up to the minute data you could schedule it to run multiple times a day and use the event timestamp column to only get data between certain times. We can set it to run right now or at some point in the future and we can set a limited number of runs if we want to. I'm going to leave the defaults here. We'll pick our destination data set which is web underscore vitals underscore materialized and the table ID which is web underscore vitals underscore summary and we use the same partitioning field as to create or replace table statement in our backfill query which was event underscore timestamp. Importantly we'll pick append to table and we'll leave the data location as default. Once we click save it will run for the first time immediately and we can click on details to see how it goes. Now this could take a few minutes so we'll move straight on to the next step. So we've got our materialized set of events with just the most important parts pulled out and ready to visualize. From here there are two routes you can take. The first is to add your materialized data as a resource in data studio and start building tables and charts from there. This gives you a lot of flexibility but it's also very time consuming. The other option is to use a pre-built connector and the great news is that the Chrome DevRel team have already built that connector. So we'll follow the link in the code lab to load the connector. We'll need to input the table location where we materialized our data. So the project ID is web vitals ga4 demo and the data set ID is web vitals materialized and finally the table name which is web vitals summary. We need to enter a project ID for billing so we're going to use the same project ID and click connect. We'll wait a few moments for that connection to be made and then we see a list of fields that the connector makes available to us. So if we click create report in this corner then create report again we will see the report in a new tab. Now the default date range has been set to last year so we'll update that to look at for example the last 14 days. If we click apply and there we have it easy to read charts about our CWV performance. We've got site-wide data here showing us the daily 75th percentile for each of our core web vitals. We've got breakouts of each CWV based on user behavior. We've got page path and debug analysis for each CWV with a minimum of 20 views in the selected time frame and we can tweak that using the settings button on each of the charts moving the slider up to see only the most visited pages. If you're tracking user revenue in your GA account you'll be able to see this in the revenue analysis page. You can customize the charts in the report by clicking edit in the top right corner. You could for example add a reference line to your time series charts to show where the thresholds for needs improvement and poor performance are. You can do this by clicking the chart when you're in edit mode selecting style in the chart editor and then add reference line. For LCP the thresholds are 2.5 seconds. So we'll add a yellow needs improvement line here. The threshold for poor is 4 seconds. So we'll add a red poor performance line here. We can see that the FID score for this site is an extremely good 11.8 milliseconds and the CLS is hovering around the 0.1 threshold of good and needs improvement. So this tells us we need to drill down into LCP performance trying to find the large elements that are loading slowly and continue to improve our CLS score finding the elements that create the largest shifts by page path and debug target and re-jigging the factors that create the biggest shifts. So there you have it. Let's review what we did. We set up the Google Analytics to BigQuery link. We added web vitals.js to some pages and then deployed it to a production setting. We checked the data was flowing through to Google Analytics and BigQuery. We materialized our data to make it faster to read and limit the amount of data we need to process. And finally we visualized the metrics in Data Studio. Now we're equipped to make changes to our site to improve user experience. Thanks for watching this workshop. Don't forget to check out the codelab for code snippets and web.dev for the most up-to-date information on core web vitals. Happy optimizing.