
同一个人早上用手机打开网站,晚上又在电脑上看了一遍,算一个 UV 还是两个?
没有唯一答案。把两台设备认成同一个人,需要登录账号或某种稳定标识;什么都不认,就只能算成两个。PV 是事件计数,UV 是在一套身份规则下得到的估计值。 讨论 UV 准不准之前,得先问清楚它把什么当成“同一个访客”。
Jekit 选择了一个很克制的口径:不设置 Cookie,不生成访客 ID,也不把本地标识发给服务器。它统计的是“同一浏览器、同一站点里的访问状态”,不是现实世界中的人。
一、PV 好算,UV 麻烦
PV(Page View)回答的是:页面被打开了多少次。
同一个人刷新五次,通常就是五个 PV。SPA 内切换路由也可以产生新的 PV,只是实现时要处理重复触发和路由竞态。这里的统计对象是一次访问事件,不必知道访问者是谁。
UV(Unique Visitor)回答的是:这些访问里有多少个不同访客。
麻烦正出在“不同”二字。统计系统必须找到一个用于去重的东西:账号、Cookie、设备指纹、IP,或者浏览器本地保存的状态。选择不同,结果也不同。
| 访问场景 | PV | Jekit 眼中的 UV |
|---|---|---|
| 同一浏览器连续刷新 5 次 | 5 | 1 |
| 同一台电脑换一个浏览器 | 2 | 2 |
| 手机访问后再用电脑访问 | 2 | 2 |
| 清除站点存储后再次访问 | 2 | 2 |
| 同一浏览器当天访问两个页面 | 站点 PV 为 2 | 站点 UV 为 1,两个页面各有 1 个页面 UV |
这不是把一个人“算错”成了两个,而是统计口径从一开始就没有承诺跨设备识别人。Jekit 宁可接受这部分误差,也不建立能长期关联访问者的身份键。
二、浏览器只回答四种状态
每次上报前,Jekit 会在浏览器里判断三件事:
- 这是本地第一次使用 Jekit 吗?
- 这是今天第一次访问这个站点吗?
- 这是今天第一次访问当前页面吗?
三个问题最后被压成一个 u8 枚举:
export enum visitorStatusOption {
NewUser_TodayNewSite_TodayNewPage = 1,
ExistingUser_TodayNewSite_TodayNewPage,
ExistingUser_ExistingSite_TodayNewPage,
ExistingUser_ExistingSite_ExistingPage,
}服务器收到的是 1 到 4 中的一个数字。它可以据此累加站点和页面的当日 UV,却拿不到一个可用于追踪访问者的 ID。下一次请求也不会携带“我是上次那个人”这样的服务端身份凭据。
这四种状态只是 10 字节上报协议 中的第一个字节。
三、去重状态留在 localStorage
浏览器总得记住“今天看过哪些页面”,否则同一页面每刷新一次都会增加 UV。Jekit 把这份状态放在当前站点的 localStorage 中,不会像 Cookie 一样随 HTTP 请求自动发送。
第一层状态固定为 15 字节:
[1 字节日期][1 字节当天页面数][13 字节布隆过滤器]日期按北京时间计算。跨天后,页面访问状态整份重建,因此“每日 UV”的日界线是北京时间零点。
这里没有随机生成的用户编号。localStorage 只保存日期、当天访问过的页面数量,以及一张无法列出原始页面地址的位图。服务器看不到这 15 字节。
四、为什么用布隆过滤器
直接保存当天访问过的 URL 最准确,但 URL 可能包含文章 ID、订单号或其他不适合长期留在本地的内容,而且列表会越写越长。
Jekit 先把规范化后的页面地址压成 32 位哈希,再用两个 16 位种子生成 7 个位置,写进 104 位的布隆过滤器。检查页面时,只要 7 个位置里有一个还是 0,就能确定它今天没出现过。
这套判断最容易混淆的地方,是“确定没出现过”和“可能出现过”并不对称。如图 1 所示,页面地址经过哈希后落到 7 个位置,浏览器只需读取这些位置当前的状态。
反过来不成立。7 个位置碰巧都被其他页面点亮时,过滤器会把一个新页面误判成“访问过”。在 104 位、7 个哈希、记录 10 个页面的条件下,理论假阳性率大约是 0.7%。
这会让页面 UV 略微偏小。为避免访问页面较多时误差迅速上升,Jekit 在超过 10 个页面后按需增加一个 108 字节的第二层过滤器。普通访客通常碰不到它,文档站和大型内容站则更可能用上。
布隆过滤器适合这里,不是因为它绝对准确,而是它用很小、定长的空间换来了一个方向明确的误差:可能少算新页面,不会把已经访问过的页面说成“一定没访问过”。
五、UV 会在哪些地方产生误差
把误差写清楚,比给出一个看起来很精确的数字重要。
5.1 会偏大的情况
- 跨设备访问:手机和电脑各自保存状态,会被视为两个访客。
- 更换浏览器:Chrome 和 Firefox 的站点存储互不相通。
- 清除站点数据:本地状态消失,下次访问会重新计数。
- 无痕窗口:独立的临时存储会形成新的统计上下文。
因此,Jekit 的累计 UV 更接近“首次出现的浏览器存储上下文数量”,不能直接解释为累计真实人数。
5.2 会偏小的情况
- 布隆过滤器假阳性:新页面可能被误判成今天已经访问过。
- localStorage 不可用:当前实现会保守地返回“已有页面”,保留 PV,但不增加这次 UV。
- 哈希碰撞:两个不同页面理论上可能得到相同的 32 位哈希,统计会合并到一起。
这些偏差不会神奇地互相抵消。它们只是方向不同,而且随访问习惯、站点规模和浏览器环境变化。
六、不追求跨设备去重,是一个产品选择
更精确的 UV 并非做不到。登录账号可以跨设备识别用户,稳定 Cookie 可以跨会话去重,设备指纹还可以在用户不登录时尝试关联浏览器。
代价也很直接:统计系统开始拥有一把能把多次访问串起来的钥匙。即使现在只拿它计数,未来也可以拿它还原路径、划分人群或延长观察窗口。
Jekit 没有保存这把钥匙。所以它能回答“今天这个浏览器是否第一次访问本站或这个页面”,却回答不了“这个人上个月还去过哪些页面”。后者对增长分析很有用,但不是 Jekit 想提供的能力。
如果你需要跨设备用户旅程、转化漏斗或精确归因,应该选择带账号体系或稳定访客标识的分析工具。如果你只想知道页面有没有人看、访问趋势怎样,又不愿建立访客档案,这套口径更合适。
七、看数据时记住口径
在统计面板里,PV 可以理解为符合上报条件的页面访问次数;每日 UV 则是在当前浏览器本地状态下去重后的估计值。
所以,更适合观察的是趋势,而不是把某一天的 127 UV 当成现实中恰好有 127 个人。今天比昨天多,某篇文章长期高于另一篇,这些相对变化通常比个位数差异更有意义。
统计工具给出的从来不是上帝视角。至少在 Jekit 这里,误差来自哪里、为什么接受它,都可以从源码里查到。
