追蹤與分析Core Web Vitals,我們需要底層用戶數據!
之前我們討論了很多關於頁面性能的問題以及如何優化Core Web Vitals的方法,但在具體落地去優化Core Web Vitals之前,我們需要能夠追蹤到非常明細的頁面性能指標,明細到不同顆粒度下例如:
- 不同國家、IP下;
- 不同頁面鏈結、頁面類型下;不同手機型號和設備廠商下;
- 不同網絡情況下:例如3g,4g,Wifi;
- 哪些異常數據導致。我之前有遇到過,在美國IP下經常因為用戶使用Realme此類低端配置的手機型號訪問我的網站,導致網站的性能指標很差。很可惜的一點是,Google並不會因此而心慈手軟,Google仍然會將此類數據作為頁面性能好壞的評估標準,所以排查異常數據、分情況優化是很有必要的;
- 用戶緯度下的訪問,這意味我們需要拿到用戶底表數據,這一點非常重要。
為能追蹤以及分析以上的明細維度的數據,我們可以利用Google Data Studio,GA4,Bigquery三者連動來完成以上的訴求。
讀者可能會有一些疑問:Pagespeed Insight/Lighthouse不是能夠直接跑出性能分數嗎?Google的CrUX Dashboard不是有現成的報表可以直接使用嗎?GA4可以直接關聯Data Studio,為什麼還需要先關聯Bigquery?以下我來一一解答:
1. 只依賴Pagespeed Insight/Lighthouse來做分析與優化性能可以嗎?
Lighthouse跑出來的是Lab Data(你自己的本地數據,也叫實驗數據)而非Field Data(用戶真實訪問的數據),真實用戶數據與你自己的本地數據的差異還是蠻大的。我們可以通過Pagespeed Insight獲取到Field Data(Pagespeed Insight的數據來源於CrUX Data),但是只能看到域名層級的聚合數據,而不能看到具體的頁面性能數據,且Pagespeed Insight的報告結果是過去28天的平均值,做了任何的優化後還需要等待28天的時間才能看到優化是否有效,28天的滯後性不利於我們快速調整和優化策略。
2. 可以直接依賴CrUX Dashboard來分析與優化性能嗎?
接入CrUX Dashboard雖然非常簡單,但是CrUX Dashboard的數據也是來源於CrUX Data,所以與Pagespeed Insight的問題一樣,不僅有28天的數據滯後性,同時也只能看整個網站的平均數據,而看不到具體頁面的性能數據。還有一個較嚴重的問題在於,CrUX Data雖然聚合了400多萬網站的真實用戶數據,但是其計算口徑與Google Search Console對於Good URL的計算口徑不一樣(具體可以前往了解Good URL的計算方式),這就是為什麼在CrUX Dashboard上網站表現的還不錯,但是在GSC上的報錯還是很多,Good URL還是沒有達到滿分。
3. 利用Google Data Studio,GA4,Bigquery追蹤高時效性的用戶底層數據
我們的確可以直接將Core Web Vitals的數據上報至GA4或者Universal Analytics,之後將其關聯至Data Studio即可。但上報至Universal Analytics的數據均為聚合過的數據,我們無法獲取到用戶層級的數據,如果需要用戶層級數據的話,需要升級為Google Analytics360,GA360付費版年費需要2萬美金,實在有點小貴(後續我會增加一篇GA4與GA360的價格和服務對比,敬請關注);那有沒有辦法可以不升級到付費版也能拿到用戶層級的數據呢?有辦法!我們將數據上報至GA4,關聯GA4與Bigquery後,按照自己的需求運行腳本,就可以產出用戶層級的數據底表了。數據底表對於後續的分析起著至關重要的作用,畢竟我們在分析75分位時,是基於每一條用戶訪問數據得出的,例如以下的表格:
創建可分析報表的數據源訴求
為追蹤和優化性能指標,歸根結底,我們的數據源和報表需要具備以下幾點:
1. 高時效性(至少每日更新)
2. 具備底層用戶層級數據
3. 數據源免費、不花錢哈哈
利用Bigquery與GA4免費獲取數據元,通過Data Studio搭建報表
利用Bigquery、GA4與Data Studio,我們可以獲取到pageview層級、用戶層級的數據,並且具備相當高的時效性(日更新),這樣可以幫助我們快速的定位到網站性能的問題有哪些。接下來我會分步驟詳細的說明如何實現,我們現在開始吧...
方法一:通過gtag上報至GA4:
這一步的上報方式只適用於gtag手動上報,如果你更熟悉GTM上報的話,可以點擊跳去下一步。如果使用gtag的方式上報,首先所有網頁都需要嵌入gtag代碼,其次需要將以下的代碼安裝在所有頁面上,放置在gtag代碼之後,截止至此上報完成。
<script id="web-vitals-debug">
/*
* Debug Web Vitals in the field
*
* Functions to add Web Vitals debug info
*
* https://web.dev/debug-web-vitals-in-the-field/
*
*/
function getSelector(node, maxLen = 100) {
var sel = '';
try {
while (node && node.nodeType !== 9) {
const part = node.id ? '#' + node.id : node.nodeName.toLowerCase() + (
(node.className && node.className.length) ?
'.' + Array.from(node.classList.values()).join('.') : '');
if (sel.length + part.length > maxLen - 1) return sel || part;
sel = sel ? part + '>' + sel : part;
if (node.id) break;
node = node.parentNode;
}
} catch (err) {
// Do nothing...
}
return sel;
}
function getLargestLayoutShiftEntry(entries) {
return entries.reduce((a, b) => a && a.value > b.value ? a : b);
}
function getLargestLayoutShiftSource(sources) {
return sources.reduce((a, b) => {
return a.node && a.previousRect.width * a.previousRect.height >
b.previousRect.width * b.previousRect.height ? a : b;
});
}
function wasFIDBeforeDCL(fidEntry) {
const navEntry = performance.getEntriesByType('navigation')[0];
return navEntry && fidEntry.startTime < navEntry.domContentLoadedEventStart;
}
function getDebugInfo(name, entries = []) {
// In some cases there won't be any entries (e.g. if CLS is 0,
// or for LCP after a bfcache restore), so we have to check first.
if (entries.length) {
if (name === 'LCP') {
const lastEntry = entries[entries.length - 1];
return {
debug_target: getSelector(lastEntry.element),
event_time: lastEntry.startTime,
};
} else if (name === 'FID') {
const firstEntry = entries[0];
return {
debug_target: getSelector(firstEntry.target),
debug_event: firstEntry.name,
debug_timing: wasFIDBeforeDCL(firstEntry) ? 'pre_dcl' : 'post_dcl',
event_time: firstEntry.startTime,
};
} else if (name === 'CLS') {
const largestEntry = getLargestLayoutShiftEntry(entries);
if (largestEntry && largestEntry.sources) {
const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
if (largestSource) {
return {
debug_target: getSelector(largestSource.node),
event_time: largestEntry.startTime,
};
}
}
}
}
// Return default/empty params in case there are no entries.
return {
debug_target: '(not set)',
};
}
</script>
<script id="web-vitals-rating">
/*
* Get Web Vital Rating
*
*/
function getRating(name, value) {
switch (name) {
case 'LCP': return calculateRating(value,2500,4000);
case 'FID': return calculateRating(value,100,300);
case 'CLS': return calculateRating(value,0.1,0.25);
case 'FCP': return calculateRating(value,2000,4000); // Page Speed Insights is 1000 and 3000, lighthouse and web.dev does 2000 and 4000
case 'TTFB': return calculateRating(value,500,1500); // CrUX Data Studio report says NI is 500ms to 1500ms
default: return '(not set)';
}
}
function calculateRating(value, good, poor) {
if (!value && value !== 0) return '(not set)';
if (value > poor) return 'poor';
if (value > good) return 'ni';
return 'good';
}
</script>
<script id="web-vitals-ga4">
/*
* Send Core Web Vitals to Google Analytics 4
*
* https://github.com/GoogleChrome/web-vitals#using-gtagjs-google-analytics-4
*
* Modified to call getRating and getDebugInfo to add extra event data
*
*/
function sendToGoogleAnalytics({name, delta, value, id, entries}) {
// Assumes the global `gtag()` function exists, see:
// https://developers.google.com/analytics/devguides/collection/ga4
gtag('event', name, {
// Built-in params:
value: delta, // Use `delta` so the value can be summed.
// Custom params:
metric_id: id, // Needed to aggregate events.
metric_value: value, // Optional.
metric_delta: delta, // Optional.
// OPTIONAL: any additional params or debug info here.
// See: https://web.dev/debug-web-vitals-in-the-field/
// metric_rating: 'good' | 'ni' | 'poor',
metric_rating: getRating(name, value), // not used by my report
// debug_info: '...',
...getDebugInfo(name, entries)
});
}
</script>
<script id="web-vitals-cdn">
/*
* Using the web-vitals script from a CDN
*
* https://github.com/GoogleChrome/web-vitals#from-a-cdn
*
* Modified to call the sendToGoogleAnalytics function on events
*
*/
(function() {
var script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals';
script.onload = function() {
// When loading `web-vitals` using a classic script, all the public
// methods can be found on the `webVitals` global namespace.
webVitals.getCLS(sendToGoogleAnalytics);
webVitals.getFID(sendToGoogleAnalytics);
webVitals.getLCP(sendToGoogleAnalytics);
webVitals.getFCP(sendToGoogleAnalytics);
webVitals.getTTFB(sendToGoogleAnalytics);
}
document.head.appendChild(script);
}())
</script>
方法二:通過GTM上報至GA4:
GTM上有Core Web Vitals的代碼範本,可以直接在GTM上選擇嵌入和使用。但此代碼範本較為通用,上報的都是一些較為基礎的指標。而我們這篇文章會教你通過GTM自定義上報多維度的指標和數據。
我假設妳已經很熟悉用GTM上報數據,接下來你需要創建一個自訂HTML代碼(Custom HTML tag),將以下的代碼複製粘貼至你的自訂HTML代碼裡。此代碼會將Core Web Vitals的所有數據都推送至數據層,代碼和創建的步驟如下所示:
1. 複製代碼,此代碼會將Core Web Vitals所需的數據推送至數據層。
<script id="web-vitals-debug">
/*
* Debug Web Vitals in the field
*
* Functions to add Web Vitals debug info
*
* https://web.dev/debug-web-vitals-in-the-field/
*
*/
function getSelector(node, maxLen) {
maxLen = maxLen || 100;
var sel = '';
try {
while (node && node.nodeType !== 9) {
var part = node.id ? '#' + node.id : node.nodeName.toLowerCase() + (
(node.className && node.className.length) ?
'.' + Array.from(node.classList.values()).join('.') : '');
if (sel.length + part.length > maxLen - 1) return sel || part;
sel = sel ? part + '>' + sel : part;
if (node.id) break;
node = node.parentNode;
}
} catch (err) {
// Do nothing...
}
return sel;
}
function getLargestLayoutShiftEntry(entries) {
return entries.reduce(function(a, b) {return a && a.value > b.value ? a : b});
}
function getLargestLayoutShiftSource(sources) {
return sources.reduce(function(a, b) {
return a.node && a.previousRect.width * a.previousRect.height >
b.previousRect.width * b.previousRect.height ? a : b;
});
}
function wasFIDBeforeDCL(fidEntry) {
var navEntry = performance.getEntriesByType('navigation')[0];
return navEntry && fidEntry.startTime < navEntry.domContentLoadedEventStart;
}
function getDebugInfo(name, entries) {
entries = entries || [];
// In some cases there won't be any entries (e.g. if CLS is 0,
// or for LCP after a bfcache restore), so we have to check first.
if (entries.length) {
if (name === 'LCP') {
var lastEntry = entries[entries.length - 1];
return {
debug_target: getSelector(lastEntry.element),
event_time: lastEntry.startTime,
};
} else if (name === 'FID') {
var firstEntry = entries[0];
return {
debug_target: getSelector(firstEntry.target),
debug_event: firstEntry.name,
debug_timing: wasFIDBeforeDCL(firstEntry) ? 'pre_dcl' : 'post_dcl',
event_time: firstEntry.startTime,
};
} else if (name === 'CLS') {
var largestEntry = getLargestLayoutShiftEntry(entries);
if (largestEntry && largestEntry.sources) {
var largestSource = getLargestLayoutShiftSource(largestEntry.sources);
if (largestSource) {
return {
debug_target: getSelector(largestSource.node),
event_time: largestEntry.startTime,
};
}
}
}
}
// Return default/empty params in case there are no entries.
return {
debug_target: '(not set)',
};
}
</script>
<script id="web-vitals-rating">
/*
* Get Web Vital Rating
*
*/
function getRating(name, value) {
switch (name) {
case 'LCP': return calculateRating(value,2500,4000);
case 'FID': return calculateRating(value,100,300);
case 'CLS': return calculateRating(value,0.1,0.25);
case 'FCP': return calculateRating(value,2000,4000); // Page Speed Insights is 1000 and 3000, lighthouse and web.dev does 2000 and 4000
case 'TTFB': return calculateRating(value,500,1500); // CrUX Data Studio report says NI is 500ms to 1500ms
default: return '(not set)';
}
}
function calculateRating(value, good, poor) {
if (!value && value !== 0) return '(not set)';
if (value > poor) return 'poor';
if (value > good) return 'ni';
return 'good';
}
</script>
<script id="web-vitals-ga4">
/*
* Send Core Web Vitals to the DataLayer
*
*/
function sendToDataLayer(metric) {
var webVitalsMeasurement = {
name: metric.name,
id: metric.id,
value: metric.value,
delta: metric.delta,
rating: getRating(metric.name, metric.value),
valueRounded: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
deltaRounded: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value)
};
var debugInfo = getDebugInfo(metric.name, metric.entries);
if(debugInfo.debug_target) webVitalsMeasurement.debugTarget = debugInfo.debug_target;
if(debugInfo.debug_event) webVitalsMeasurement.debugEvent = debugInfo.debug_event;
if(debugInfo.debug_timing) webVitalsMeasurement.debugTiming = debugInfo.debug_timing;
if(debugInfo.event_time) webVitalsMeasurement.eventTime = debugInfo.event_time;
dataLayer.push({
event: 'coreWebVitals',
webVitalsMeasurement: webVitalsMeasurement
});
}
</script>
<script id="web-vitals-cdn">
/*
* Using the web-vitals script from a CDN
*
* https://github.com/GoogleChrome/web-vitals#from-a-cdn
*
* Modified to call the sendToGoogleAnalytics function on events
*
*/
(function() {
var script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals';
script.onload = function() {
// When loading `web-vitals` using a classic script, all the public
// methods can be found on the `webVitals` global namespace.
webVitals.getCLS(sendToDataLayer);
webVitals.getFID(sendToDataLayer);
webVitals.getLCP(sendToDataLayer);
webVitals.getFCP(sendToDataLayer);
webVitals.getTTFB(sendToDataLayer);
}
document.head.appendChild(script);
}())
</script>
2. 點擊新增代碼
3. 選擇自訂HTML
4. 將以上複製的代碼粘貼至HTML框內,並將此代碼命名為CWV Bigquery
你還需要在代碼配置裡設置代碼觸發順序,確保GA4 Configuration代碼觸發是在CWV Bigquery之前的。此處的GA4 Configuration在後續會提到,這裡是指你還需要單獨再創建一個代碼,將pageview等事件全部上報至所指定的measurement ID,而我們不希望在發生pageview事件之前就將Core Web Vitals事件上報,所以需要設定上報的先後順序。
5. 配置變數。這些變數也會被推送至數據層。
複製以下的變數名稱以及對應的資料層變數名稱,將其粘貼至所創建的變數裡:
變數名稱(Variable Name) | 資料層變數名稱(Data Layer Variable Name) |
---|---|
DLV – webVitalsMeasurement.name | webVitalsMeasurement.name |
DLV – webVitalsMeasurement.id | webVitalsMeasurement.id |
DLV – webVitalsMeasurement.value | webVitalsMeasurement.value |
DLV – webVitalsMeasurement.delta | webVitalsMeasurement.delta |
DLV – webVitalsMeasurement.valueRounded | webVitalsMeasurement.valueRounded |
DLV – webVitalsMeasurement.deltaRounded | webVitalsMeasurement.deltaRounded |
DLV – webVitalsMeasurement.debugTarget | webVitalsMeasurement.debugTarget |
DLV – webVitalsMeasurement.debugEvent | webVitalsMeasurement.debugEvent |
DLV – webVitalsMeasurement.debugTiming | webVitalsMeasurement.debugTiming |
DLV – webVitalsMeasurement.eventTime | webVitalsMeasurement.eventTime |
DLV – webVitalsMeasurement.rating | webVitalsMeasurement.rating |
配置變數的步驟如下所示:
- 步驟一:點擊變數,點擊新增變數
- 步驟二:新增資料層變數
- 步驟三:複製粘貼變數名以及資料層變數名。
以上我提供了11個變數名以及其對應的資料層變數名,所以你需要創建11個變數。
6. 創建觸發事件:
創建自訂事件,事件名稱為Event-coreWebVitals,框內填寫coreWebVitals。
7. 創建GA4 Configuration Tag
現在,我們需要創建一個新的tag,將這個tag命名為“GA4-Event-core Web Vitals”,通過這個tag我們會將剛創建的觸發條件Event-coreWebVitals與所需的參數都上報至我們指定的GA4 measurement ID,即剛才所提的GA4 Configuration tag。這裡需要填寫的值和參數如下:
- 代碼類型:Google Analytics(分析):GA4事件
- 設定代碼:GA4 Configuration。GA4 Configuration如下所示,選擇Google Analytics(分析):GA4設定(GA4設定和GA4事件的區別是什麼),輸入你指定的Measurement ID
- 事件名稱:{{DLV – webVitalsMeasurement.name}}
- 參數名稱以及值:以下將指定具體要發送什麼變數
Parameter name | Value |
---|---|
metric_name | {{DLV – webVitalsMeasurement.name}} |
metric_id | {{DLV – webVitalsMeasurement.id}} |
metric_value | {{DLV – webVitalsMeasurement.value}} |
value | {{DLV – webVitalsMeasurement.delta}} |
metric_delta | {{DLV – webVitalsMeasurement.delta}} |
debug_target | {{DLV – webVitalsMeasurement.debugTarget}} |
debug_event | {{DLV – webVitalsMeasurement.debugEvent}} |
debug_timing | {{DLV – webVitalsMeasurement.debugTiming}} |
event_time | {{DLV – webVitalsMeasurement.eventTime}} |
metric_rating | {{DLV – webVitalsMeasurement.rating}} |
- 觸發條件選擇剛剛創建好的Event-core Web Vitals
以上,我們的GA4 tag就創建好了,如下所示:
- 配置頁面類型和聯網類型(Optional,選擇性配置)
你也可以在GA4 Configuration tag裡設置頁面類型(page_type) 與聯網類型(effective_connection_type),之後在關聯Bigquery時,可以獲取到Wi-Fi、3g、4g、哪些類型的頁面影響你的性能。設定page_type和effective_connection_type之前,你需要將數據推送至數據層、創建好變數,然後再在GA4 Configuration裡做關聯。
在創建頁面類型變數時,可以按照變數裡的網址->路徑來區分頁面類型;在創建聯網類型變數時,需要利用自訂Javascript(Custom Javascript),講以下的代碼複製粘貼進去:
function () {
var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection && connection.effectiveType) return connection.effectiveType;
return 'unknown';
}
緊接著,你可以在GA4 Configuration裡關聯page_type與effective_connection_type:
8. Preview:預覽模式檢查是否上報成功
以上,我們已經完成了上報core web vitals事件上報至GA4的整體鏈路,不要忘記在GTM預覽模式檢查一下是否成功上報以上的tag,如果預覽模式成功上報的話,那麼可以前往GA4 property中的實時報表查看是否收到事件!接下來,我們需要關聯GA4和Bigquery,產出最終的報表,並且將數據報表套用到現有的Data Studio模板上。具體的操作流程參考【下篇】通過Google Data Studio,GA4,Bigquery來追蹤Core Web Vitals,拿到用戶明細數據。