淘宝的鱼相在 2012 年 8 月份发表了一篇文章,里面讲述了他们通过一个月的数据采集试验,得到的结果是:如果在浏览器的本页面刷新之前发送打点请求,各浏览器都有不同程度的点击丢失情况,具体点击丢失率统计大家请看下图(数据日期为 2012 年 7 月份):
从图中可以看出,chrome,safari 这类 webkit 内核的浏览器在本页刷新之前发送打点,导致的丢失最为严重,分别为 61%,76%,而 ie8 丢失的情况最少,为7%。 (具体大家可以参看此文:http://ued.taobao.com/blog/?p=116 )
原因分析:
基于这种现象的产生,抛开打点服务器的处理性能和网络网速因素,我们从前端的角度来分析一下:
当点击浏览器的当前页面某个元素后,当前页面创建 image 对象,并把这个 image 对象的 src 设为指向打点服务器的 url,发送打点请求。接着,当前页又向页面服务器发送页面跳转请求。假设页面服务器很快就返回了请求的 html, 则浏览器窗口接收到页面服务器的响应后,开始卸载保存在用户内存中的跟当前页面的有关的所有元素,包括刚才创建的用来向打点服务器发送打点的 image 对象。假设这时打点服务器对这个 image 发送的打点请求还在处理阶段,比如正在向数据库插入数据,还没有做 response 响应。这时,由于页面卸载了,image 对象被销毁了,该对象发送的 src 请求也就被终止了。导致这个打点失败了。类似于垃圾回收机制的影响。如果刚创建的 image 元素立即被内存垃圾回收了,而这个图片的 HTTP 请求尚未建立,那么在被回收时这个请求就会被取消,导致打点并没有真正发出。一个 http 请求的成功,必须是与后端服务器握手的成功。根据 DW 的胡大军检测到的数据,后端服务器监测到 3 次 tcp 协议的请求后,http 请求就被终止了。从这种现象上说明,浏览器在页面跳转之前确实发送了打点请求,由于页面跳转过快,发送打点的 image 对象因页面卸载而销毁了,请求也就被终止了。
具体示意图如下:
搜索端应用 searchweb 曾经做过试验,如果 searchweb 端响应延迟 100ms 后返回 html 数据,这种在页面卸载之前发送的请求打点(我们这里称它为 Beforeunload 打点)的回收率就明显上升,差不多得到了 100% 的回收率。页面服务器响应时间越长,Beforeunload 打点丢失率越低。因为页面服务器响应时间变长后,打点服务器有足够的时间处理打点请求, 然后返回给 image 对象做 response 响应。在本页面卸载刷新之前,image 对象的 src 请求已经成功得到了响应。这时再销毁该对象,对打点丢失已经没什么影响了。可以说,只要用来打点的 image 对象在打点成功之前没有被销毁,打点就不会丢失。如果链接跳转的方式是新窗口打开的,也就不会发生打点丢失的情况。因为当前页创建的打点 image 对象被没有被销毁。
解决方案
找到了 Beforeunload 打点丢失的原因,我们再来思考一下解决方案:
因为页面创建的 image 对象在打点请求还没响应之前就被卸载了。如果单纯提升打点服务器性能,确实能加快 image 打点请求的响应返回,减少 Beforeunload 打点丢失情况。问题是,打点服务器要提升多少性能才是合适的?其实是页面服务器(应用服务器)跟打点服务器的一场竞赛。如果页面服务器性能大大提升了,而打点服务器没有提升,那么差距又拉大了。 又或者像 searchweb 做的试验那样,页面服务器(应用服务器)做延迟响应处理,这样 Beforeunload 打点丢失就能得到有效遏制。问题是这其实是对用户体验的极大伤害。现如今大家都在拼命提升应用页面服务器的性能,让页面能加载的更快,用户使用网页感觉更流畅。为了能在服务后端打上点,我们却要自己的应用延迟响应,跟我们提升用户体验的目标背道而驰。要知道,任何软件的应用,良好的用户体验是第一位的。所以,此路不通也。
那有没有一种更好的办法?比如在页面刷新之前缓存打点数据,页面卸载刷新后,把原来页面刷新之前保存的打点数据取出来,再打点出去?这样,刷新后的页面打点跟点击 pv 基本上就是1:1 了,因为 pv 的计算也是根据跳转后的页面 url 来统计的。答案是:还真有。
在浏览器页面被卸载的过程中,除了 window.name 属性对象外,其他对象元素都被销毁了。所以我们在页面卸载之前把请求打点的数据保存在 window.name 中。然后,页面刷新后,再取出这个 window.name 对象中保存的打点数据,发送打点请求。
如果单存要存储打点数据,让该数据在页面刷新之后仍然能够被访问,有 cookie 对象,高级浏览器还支持 localStorage,为什么只推荐 window.name ? 原因如下:
- window.name 没有类似于 cookie,localStorage 的跨域问题。如果从 www.taobao.com 跳到 www.tmall.com,天猫的页面仍然能够得到在淘宝页面设置的 window.name 的值。而 cookie 只能跨主域。Localstorage 则不能跨域,连跨主域都不行,所以,在上面的场景中,如果使用 cookie 或者 localstorage,天猫页面无法获取淘宝页面设置的 cookie 或者 localstorage;
- Window.name 所有浏览器都支持,无论是 webkit 内核还是 IE 系列,所有浏览器都一概支持,而且表现一致。
- 页面刷新打点完毕后,原来被保存的打点数据已经没有用了,数据保存形式应该是临时性质的。cookie,localstorage 对象的设计是用来存储持久数据的,而 window.name 的设计,就是为保存页面临时数据的。如果页面窗口关掉了,内存回收了 widow 对象,window.name 也就不存在了。两者的需求与用途一致。
如此看来,用 window.name 来存储 Beforeunload 打点请求的数据确实是个不错的主意,但是还有一个问题:如果 window.name 属性对象已经被应用程序使用了,也就是 window.name 已经被应用设为某个值了,这时我们再修改这个 window.name,岂不是对应用程序造成了破坏?
答案是:没有关系。我们在页面跳转前,拼接完打点串后,可以这样写
window.name=window.name+”|%”+logURL;
其中,logURL 是要打点的请求串。通过这样设置 window.name,原来应用程序的 window.name 的数据仍然被保存着。在页面跳转后的打点 js 中,可以这样写打点代码:
var windowUrl=$.trim (window.name); if(windowUrl && windowUrl.indexOf ("|%")!==-1){ var urlArray=windowUrl.split ("|%"),orginName=urlArray[0]; windowUrl=urlArray[1]; //把 window.name 归还给应用程序 window.name=orginName; //发送打点请求 sendByImg (windowUrl); }
上面这段页面曝光打点的代码,放在标签里面,类似于 1688 中文站的 beacon.js,专门用来打点。因为打点代码在 head 标签里,一般在所有应用代码之前执行。(一般应用代码在 domready 的时候执行)所以在该代码执行完后,window.name 已经被设置为原来应用的值了。这样对应用造成的破坏也就不存在了。
解决方案的示意图如下:
解决方案数据验证效果
蓝色为 Beforeunload 打点,红色为用 window.name 先存储打点数据,页面刷新跳转后,再从 window.name 取回打点数据,曝光打出的点。 Chrome 浏览器下的平均数据如下:
可以看到,通过 window.name 的解决方式,打点效果提升了 13%。
IE 平台下的总平均回收效果如图(包括 ie6,ie7,ie8,ie9,ie10 总和):
IE 系列各浏览器的平均回收效果如图:
(注:目前中文站的用到的 fdev-min.js 依赖的 jQuery1.7.2 的 attr 方法有个 bug,当 IE10 浏览器的 ie7 文档模式下调用 attr 方法时,程序会抛异常,导致了 window.name 不能被赋值。上图中,IE10 有可能因为受此影响,表现不如 Beforeunload 打点。关于这个 jQuery bug 的详细介绍,有兴趣的同学,可以看看这篇 jQuery 官网的 bug 帖 http://bugs.jquery.com/ticket/12577)
方案总结
总体而言,新方案跟无痕打点的 Beforeunload 打点方式时相比,在 ie 上略有提升,chrome 浏览器上,提升最为明显,相比原来提升了 13%。这种方案还是很有效果的。