Google Analytics 优化 @ WordPress

Google Analytics 是一款优秀的免费流量统计服务,集成也简单方便,直接引入一段脚本即可,也就这么稀里糊涂的用了好多年。因为自己在写一款自用的 WordPress 主题(也就是本博客使用的这款),一直在纠结页面访问速度这事儿,于是无关痛痒的事情也会变得有紧要起来,至于为啥且听我慢慢道来:

为何要优化

作为一个懒人,推崇的是“不要过度优化”,但官方引入 Google Analytics 统计的方式确实存在如下问题:

  1. Google Analytics 服务可用性问题
  2. 影响页面加载速度

可用性问题

值得欣慰的是,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 昏的切图狗,这是不可容忍的!

加速方案

以下是我了解到的几种方案,各有优劣,我会简单分析下,最后在详细介绍下我自己的方案,以及原因。主要的优化方案逃不过两大种:纯前端处理以及后端转发。

自行托管脚本

先说下怎么做吧:

  1. 先将 JS 脚本 托管到到到自己的服务器上或者靠谱的第三方服务,JS 脚本地址为:https://www.googletagmanager.com/gtag/js
  2. 分别将 https://example.com/xx.jsUA-xxxxxxxx-x 替换为自己的 JS 脚本地址于 Google Analytics ID。
  3. 再将统计代码部署到博客上即可。

    <!-- 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>

这么做好的好处是托管到本地后,节约了额外的域名请求时间,还可以自行控制统计脚本的缓存时间,也不必担心统计脚本不可用了,不过仍然存在如下问题:

  1. 仍然存在被屏蔽的风险:如果用户的客户端屏蔽插件用了特性检测,或者将 Google Analytics 数据接收地址屏蔽,该风险仍然存在。
  2. Google Analytics 统计脚本是不断更新的,另行缓存在本地服务器上也需做同步的维护操作。
  3. 官方的 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 的 rewriteproxy 功能:

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 参数参考,自行对照一下即可知晓需要发送的参数。有几点需要说明下:

  1. UA 我是没有通过前端获取的,因为后端从请求中也能拿到,无需重复发送。
  2. 发送数据的方式优先使用 navigator.sendBeacon,具体优势 MDN 有解释,浏览器不支持会降级到 XMLHttpRequest
  3. 发送的数据格式我偷懒用了 FormData, IE 10 以下版本是不支持的(所以我为什么要支持 IE 10)。
  4. 每次请求都会添加一个时间戳 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 可统计页面的错误信息,这里代码就不贴了,无法是监听 errorunhandledrejection 并且进行上报,上报数据字段参考异常这一节。

代码汇总

最终效果

直接打开 Chrome Dev Tool 看效果就好啦,过段时间我将 Google Analytics 的性能统计贴上来,给大家参考。

参考

天下文章一大抄,感谢以下被我抄袭的博客与资料:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注