Google Analytics 是一款优秀的免费流量统计服务,集成也简单方便,直接引入一段脚本即可,也就这么稀里糊涂的用了好多年。因为自己在写一款自用的 WordPress 主题(也就是本博客使用的这款),一直在纠结页面访问速度这事儿,于是无关痛痒的事情也会变得有紧要起来,至于为啥且听我慢慢道来:
为何要优化
作为一个懒人,推崇的是“不要过度优化”,但官方引入 Google Analytics 统计的方式确实存在如下问题:
- Google Analytics 服务可用性问题
- 影响页面加载速度
可用性问题
值得欣慰的是,Google Analytics 统计相关的 www.google-analytics.com 的服务器在国内没有被墙,实锤被墙的只是管理后台(毕竟挂在的是在 google.com 上)。站长之家与 17ce 的监测结果也印证了:Google Analytics 的统计搜集服务在国内是堪用滴,更准确的说是并非完全不可用。但是保不齐某些地区或者 the Great Wall 抽风,并且本人之前就遇到过。只能姑且这个结论定为国内使用 Google Analytics 可能存在可访问性问题。
目前统计脚本地址升级成了:https://www.googletagmanager.com/gtag/js 这个地址,但是不影响上述结论。
更重要的一点是,Google Analytics 需要收集用户浏览信息,势必与一些人不愿意“隐私”被获取的意愿相冲突。所以 Google Analytics 的统计脚本在一些用户的浏览器中会被人为 Ban 掉,导致这部分用户压根无法被统计。
影响页面速度
集成 Google Analytics 需要引入一段约为 80KB(BR 格式压缩后约为 30KB)的第三方 JavaScript 脚本,而且缓存时间被设置为超短的 max-age=900
。尽管已经优化为了异步加载,不再阻塞页面渲染,但是引入一个第三方脚本,需要增加 DNS 解析、建立 TCP 链接、额外的资源加载时间,缓存时间过短这些硬伤,这些都是要被各种网页性能检测工具拖出来打重 30 大板的!作为一个誓必要将博客 PageSpeed Insights 评分优化为 100
昏的切图狗,这是不可容忍的!
加速方案
以下是我了解到的几种方案,各有优劣,我会简单分析下,最后在详细介绍下我自己的方案,以及原因。主要的优化方案逃不过两大种:纯前端处理以及后端转发。
自行托管脚本
先说下怎么做吧:
- 先将 JS 脚本 托管到到到自己的服务器上或者靠谱的第三方服务,JS 脚本地址为:https://www.googletagmanager.com/gtag/js
- 分别将
https://example.com/xx.js
与UA-xxxxxxxx-x
替换为自己的 JS 脚本地址于 Google Analytics ID。 -
再将统计代码部署到博客上即可。
<!-- Global site tag (gtag.js) - Google Analytics --> <script async src="https://example.com/xx.js?id=UA-xxxxxxxx-x"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-xxxxxxxx-x');
</script>
这么做好的好处是托管到本地后,节约了额外的域名请求时间,还可以自行控制统计脚本的缓存时间,也不必担心统计脚本不可用了,不过仍然存在如下问题:
- 仍然存在被屏蔽的风险:如果用户的客户端屏蔽插件用了特性检测,或者将 Google Analytics 数据接收地址屏蔽,该风险仍然存在。
- Google Analytics 统计脚本是不断更新的,另行缓存在本地服务器上也需做同步的维护操作。
- 官方的 Google Analytics 统计脚本还是有些大,应该夹带了不少私货。
使用简化版统计脚本
既然大家嫌弃 Google Analytics 太大,咱们简化一下不就好了,恰巧有人提供了这样的服务:https://minimalanalytics.com/,压缩完才 1.5kB、Gzip 之后会更小。由于代码非常精简,这里也就贴下,省的再去打开原站复制粘贴了。
<script>
(function(a,b,c){var d=a.history,e=document,f=navigator||{},g=localStorage,
h=encodeURIComponent,i=d.pushState,k=function(){return Math.random().toString(36)},
l=function(){return g.cid||(g.cid=k()),g.cid},m=function(r){var s=[];for(var t in r)
r.hasOwnProperty(t)&&void 0!==r[t]&&s.push(h(t)+"="+h(r[t]));return s.join("&")},
n=function(r,s,t,u,v,w,x){var z="https://www.google-analytics.com/collect",
A=m({v:"1",ds:"web",aip:c.anonymizeIp?1:void 0,tid:b,cid:l(),t:r||"pageview",
sd:c.colorDepth&&screen.colorDepth?screen.colorDepth+"-bits":void 0,dr:e.referrer||
void 0,dt:e.title,dl:e.location.origin+e.location.pathname+e.location.search,ul:c.language?
(f.language||"").toLowerCase():void 0,de:c.characterSet?e.characterSet:void 0,
sr:c.screenSize?(a.screen||{}).width+"x"+(a.screen||{}).height:void 0,vp:c.screenSize&&
a.visualViewport?(a.visualViewport||{}).width+"x"+(a.visualViewport||{}).height:void 0,
ec:s||void 0,ea:t||void 0,el:u||void 0,ev:v||void 0,exd:w||void 0,exf:"undefined"!=typeof x&&
!1==!!x?0:void 0});if(f.sendBeacon)f.sendBeacon(z,A);else{var y=new XMLHttpRequest;
y.open("POST",z,!0),y.send(A)}};d.pushState=function(r){return"function"==typeof d.onpushstate&&
d.onpushstate({state:r}),setTimeout(n,c.delay||10),i.apply(d,arguments)},n(),
a.ma={trackEvent:function o(r,s,t,u){return n("event",r,s,t,u)},
trackException:function q(r,s){return n("exception",null,null,null,null,r,s)}}})
(window,"XX-XXXXXXXXX-X",{anonymizeIp:true,colorDepth:true,characterSet:true,screenSize:true,language:true});
</script>
使用方式:
需要将
XX-XXXXXXXXX-X
替换为自己的 Google Analytics 统计 ID。
由于发送数据方式是自行实现的,不大容易被广告屏蔽插件等通过特征监测到进而被屏蔽掉。不过缺点嘛,除了继承上述自行托管脚本的缺点外,还多了一点:由于统计脚本精简过,只提供了最基本的数据统计功能,某些 Google Analytics 的功能,比如与 Google Adsenes 的集成、与 Adwords 的继承、用户画像等功能,如果你很介意的话,那打住,就不用继续往下看了。
正向代理
如果将数据发送到 Google Analytics 服务器行为放在客户端去做,需要确保客户端能与 Google Analytics 可联通性,客户端的情况各种各样,这是我们很难确保的。将数据发送行为转移到服务端来做,问题就简化了——我们只需要确保服务端与 Google Analytics 可联通即可。
由于我的博客是 LAMP 架构,这里以 Apache 的配置方法为例:
首先启用 Apache 的 rewrite
与 proxy
功能:
a2enmod rewrite
a2enmod proxy
a2enmod proxy_http
service apache2 restart
修改对应的 apache 配置:
vi /etc/apache2/sites-enabled/000-default.conf
将以下配置增加到 Apache 的配置文件中:
<Location /ga>
RewriteEngine On
RewriteCond %{QUERY_STRING} ^(.+)$
RewriteRule ^/ga/collect$ /collect?%1&uip=%{REMOTE_ADDR} [L]
ProxyPass http://www.google-analytics.com
</Location>
我们以使用简化版统计脚本为例,最后需将脚本中指向 https://www.google-analytics.com/collect 的地址替换为 https://your_blog_url/ga。
最后记得重启 apache 服务器,之后即大功告成!
service apache2 restart
本部分内容参考自 http://www.mak-blog.com/apache-proxy-google-analytics
集成至 WordPress 主题
使用 Apache 正向代理转发的方案最终我没有采纳,因为需要修改相关的服务器配置。优化 Google Analytics 加载速度也是本博客主题性能优化的一环,我秉持着开箱即用的目标,显然我会将此功能集成至博客主题中。
首先是前端数据收集这块我也就自己写了,Google 提供了 Measurement Protocol 参数参考,自行对照一下即可知晓需要发送的参数。有几点需要说明下:
- UA 我是没有通过前端获取的,因为后端从请求中也能拿到,无需重复发送。
- 发送数据的方式优先使用
navigator.sendBeacon
,具体优势 MDN 有解释,浏览器不支持会降级到XMLHttpRequest
。 - 发送的数据格式我偷懒用了
FormData
, IE 10 以下版本是不支持的(所以我为什么要支持 IE 10)。 - 每次请求都会添加一个时间戳
t
,为了防止客户端缓存。
具体代码如下,当然你用上文中的简化版统计脚本也是可行的,不过得记得改下数据接收地址:
const TRACK_URL = "/wp-json/wp_theme_pure/v1/ga";
/**
* 借助 WordPress 转发到 Measurement Protocol,解决 Google 统计容易被屏蔽的问题
* https://developers.google.com/analytics/devguides/collection/protocol/v1
*/
class Track {
/**
* 初始化配置,默认发送 pageView、timing、exception
* @param {*} config - 需要发送的数据类型
*/
constructor(config = {}) {
const { value } = document.querySelector('#googleAnalyticsId');
this.logPageView = config.logPageView !== false;
this.canSend = !!value;
const self = this;
if (this.logPageView === true) {
self.doLogView();
}
}
/**
* 获取 组合好的 FormData
* 不支持 IE
* @param {Object} data
*/
genFormData(data) {
const screen = window.screen;
//基本数据,每次请求都会发送
// https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters
const basicDataPackage = {
v: 1,
// z: new Date() * 1,
ci: location.hash,
// tid: "", // tracking id
// cid: "", // client id
// uid: "", // user id
// uip: '', // user ip
ul: navigator.language.toLowerCase(), // user language
// t: "pageview", // “pageview”、“screenview”、“event”、“transaction”、“item”、“social”、“exception”、“timing”
// fl: "", // flash version
// je: 0, // java version
// ua: navigator.userAgent, // user agent
dh: location.host, // document host
ds: "web",
dp: window.location.pathname,
// dclid: "", // 指定 Google 展示广告 ID
dl: encodeURIComponent(location.href), // document location
dt: encodeURIComponent(document.title),
dr: encodeURIComponent(document.referrer), // referrer
de: (document.characterSet || document.charset || document.inputEncoding).toLowerCase(),
// gclid: "", // 指定 Google Ads ID
sd: `${screen.colorDepth}-bit`, // screen depth
sr: `${screen.width}x${screen.height}`, // screen resolution
vp: `${screen.availWidth}x${screen.availHeight}` // visible part
};
const payload = new FormData();
Object.keys(basicDataPackage).forEach((key, index) => {
payload.set(key, basicDataPackage[key]);
});
if (data && typeof data === "object") {
Object.keys(data).forEach((key, index) => {
payload.set(key, data[key]);
});
}
return payload;
}
/**
* 发送数据,如果不支持 sendBeacon 会降级到 XMLHttpRequest
* @param {*} data
*/
send(data) {
if (!this.canSend) {
return false;
}
const payload = this.genFormData(data);
const url = `${TRACK_URL}?t=${new Date() * 1}`;
if (navigator.sendBeacon) {
navigator.sendBeacon(url, payload);
} else {
const xhr = new XMLHttpRequest();
xhr.open('post', url);
xhr.onload = () => {
console.log('send success');
};
xhr.send(payload);
}
}
doLogView() {
const data = {
t: 'pageview'
};
this.send(data);
}
}
export default Track;
因为数据转发要集成进 WordPress 主题,正好可以使用 WordPress 的 REST API。我这里定义的数据接收地址为:/wp-json/wp_theme_pure/v1/ga
。那么先编辑主题 function.php
文件,定义好 API 路径。
register_rest_route('wp_theme_pure/v1', '/ga', array(
'methods' => WP_REST_Server::ALLMETHODS,
'callback' => function () {
return array(
'msg' => 'ok',
);
}),
true);
在处理数据转发之前,我们还得干两件事:首先是生成 UUID。Measurement Protocol 接收的 cid
字段为 UUID v4 标准,create_uuid()
便是用来生成合格的 UUID 的。
function create_uuid() {
$str = md5(uniqid(mt_rand(), true));
$uuid = substr($str,0,8) . '-';
$uuid .= substr($str,8,4) . '-';
$uuid .= substr($str,12,4) . '-';
$uuid .= substr($str,16,4) . '-';
$uuid .= substr($str,20,12);
return $uuid;
}
用滴时候呢,先检查下请求的 Cookies 中不带 track_uuid
,如果没带呢就调用 create_uuid()
生成一个随机的 UUID 并将其种到客户端中。如果请求中携带对应的 Cookies,那说明是老访客,直接取用即可。
const PURE_THEME_TRACK_UUID_KEY = 'track_uuid';
if (!isset($_COOKIE[PURE_THEME_TRACK_UUID_KEY])) {
$uuid = create_uuid();
setcookie(PURE_THEME_TRACK_UUID_KEY, $uuid , time()+368400000);
}else{
$uuid = $_COOKIE[PURE_THEME_TRACK_UUID_KEY];
}
另外 Measurement Protocol 接收 uip
,这个值为客户端的 IP, 这段代码是我从网上找来的,用于获取用户的真实 IP,即便用户使用了代理服务。
function get_real_ip() {
static $ip = '';
$ip = $_SERVER['REMOTE_ADDR'];
if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
$ip = $_SERVER['HTTP_CDN_SRC_IP'];
} elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
}
return $ip;
}
这里再啰嗦下我在在后端获取的字段:
tid
就是 Google Analytics 的统计 ID,我这在其他地方有一个设置的入口,这里取用即可cid
客户端的 UUID,这里不赘述ua
及是客户端的 UserAgent,我这里不用前端取,是为了减小请求的数据大小,毕竟后端也能取就无需前端发送了uip
客户端 IP 地址uid
用户 ID,我这里拿了评论用户的 E-mail,作为用户标识。
$_REQUEST['tid'] = get_option('pure_theme_google_analytics_id');
$_REQUEST['cid'] = $uuid;
$_REQUEST['ua'] = $_SERVER['HTTP_USER_AGENT'] ;
$_REQUEST['uip'] = get_real_ip();
$user_email = $_COOKIE[ 'comment_author_email_' . COOKIEHASH ];
if ($user_email) {
$_REQUEST['uid'] = $user_email;
}
接下来就是发送数据了,发送一个请求给 Google Analytics 的 Measurement Protocol 了,我这里用了 curl,我开启了 fastcgi_finish_request
实现非阻塞,我的服务端真好也是开启 PHP-FPM 模式的。至于 curl 中参数设置,文档在这里,由于本人不是很懂 PHP,如果参数乱用,还请指出斧正,不胜感激。
if (function_exists("fastcgi_finish_request")) {
fastcgi_finish_request(); // 对于 fastcgi 会提前返回请求结果,提高响应速度。
}
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => $post_data,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_HTTPHEADER => array(
'Accept-Encoding'=> 'gzip',
'cookie'=> $_COOKIE,
'User-Agent' => $_SERVER['HTTP_USER_AGENT'] . '',
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
请求头我也处理成 204
,使得前端请求无需等待服务端返回内容,然后就是禁止请求缓存,前端发送服务算也是加了时间戳,同样的目的。
header('status: 204');
header('cache-control: no-cache, no-store, must-revalidate');
header('pragma: no-cache');
除此之外,就是将获取到的数据简单凭借成合法的格式,发送至 https://www.google-analytics.com/collect 最终代码在这里
至于发送数据的正确性保证,大家可以使用官方提供的这个工具进行测试:https://ga-dev-tools.appspot.com/hit-builder/?hl=zh_cn
采集性能信息
Google Analytics 默认还会统计页面的性能数据,不过其采取的是抽样的方式,我们也可以自行收集对应的数据发送过去,这样就能验证我的一些优化手段是否有效了。其所需要的参数文档在此。
具体的收集指标如下:
plt
: 页面加载时间dns
: DNS 解析用时pdt
: 页面下载用时rrt
: 重定向用时tcp
: TCP 连接用时srt
: 服务器响应用时dit
: DOM Interactive 用时clt
: Content Load 用时
以下代码参考自:https://blog.skk.moe/post/cloudflare-workers-cfga/
class Track {
// ...
/**
* 获取页面性能信息
*/
static getTimingData() {
if (!window.performance || !window.performance.timing) {
return null;
}
const t = window.performance.timing;
let times = {};
// 页面加载完成的时间
// 从页面开始载入到绑定在 load 事件上的函数全部执行完毕
times.PageLoadTime = t.loadEventEnd - t.navigationStart;
// DOM 用时(包括资源加载和 DOM 树的解析和构建)
times.DOMReady = t.domComplete - t.responseEnd;
// DOM 交互用时
times.DOMInteractiveTime = t.domInteractive - t.domLoading;
// 重定向用时
times.RedirectTime = t.redirectEnd - t.redirectStart;
// DNS 用时
times.DNSTime = t.domainLookupEnd - t.domainLookupStart;
// TTFB 用时
// 注意这里的 TTFB 用时包括了 TCP、SSL、DNS 时间
times.TTFBTime = t.responseStart - t.navigationStart;
// 服务器响应用时
// 同时这也是 Chrome Dev Tools 的对 TTFB 的定义
times.ServerResponseTime = t.responseStart - t.requestStart;
// 页面下载时间
times.PageDownloadTime = t.responseEnd - t.responseStart;
// 从页面加载到 DOMContentLoaded 用时
times.ContentLoadingTime = t.domContentLoadedEventStart - t.navigationStart;
// 在 load 事件上的耗时
times.LoadEventTime = t.loadEvent = t.loadEventEnd - t.loadEventStart;
// 在 TCP 和 SSL 上的耗时
times.TCPTime = t.connectEnd - t.connectStart;
let ga_data = {
// 这里 plt 统计的是到 load 事件开始的时间
plt: t.loadEventStart - t.navigationStart,
dns: times.DNSTime,
pdt: times.PageDownloadTime,
rrt: times.RedirectTime,
tcp: times.TCPTime,
srt: times.ServerResponseTime,
dit: times.DOMInteractiveTime,
clt: times.ContentLoadingTime
};
return ga_data;
}
// ...
}
export default Track;
错误信息上报
同样 Google Analytics 可统计页面的错误信息,这里代码就不贴了,无法是监听 error
与 unhandledrejection
并且进行上报,上报数据字段参考异常这一节。
代码汇总
- 最后贴下 PHP 代码:https://github.com/ihuguowei/Pure/blob/develop/functions.php#L499
- 前端脚本代码:https://github.com/ihuguowei/Pure/blob/develop/assets/scripts/track.js
最终效果
直接打开 Chrome Dev Tool 看效果就好啦,过段时间我将 Google Analytics 的性能统计贴上来,给大家参考。
参考
天下文章一大抄,感谢以下被我抄袭的博客与资料: