har

HAR 工具

HAR 文件是浏览器在网络上所见一切的 JSON 记录:它的结构长什么样、七个计时阶段各自量的是什么,以及在把它交给别人之前必须先脱敏掉哪些东西。

2 工具

§01 领域指南

HAR 是通信记录,不是抓包

HAR(HTTP Archive)文件是一份 JSON 文档,描述浏览器在网络上观察到了什么。所有 有价值的内容都在 log.entries[] 里:每个元素对应一组请求/响应,携带方法、URL、 头、cookie、大小、逐阶段的计时拆分,以及——有时候——正文。格式是 HAR 1.2,一份 从未被批准的 W3C 草案,但 Chrome、Firefox、Safari、Charles 和 Fiddler 输出的方言 彼此足够接近,文件可以在它们之间互相搬运。

真正能省下几个小时的思维模型是:这份文件是浏览器自己的网络栈事后写出来的,它保存的 是那个栈的汇总,而不是线路上发生的事。没有 TLS 握手记录,没有 HTTP/2 帧,没有 DNS 报文,只有网络栈选择上报的那些时长。DevTools 打开之前发出的请求不在里面;除非开了 Preserve log,上一次导航也不在;命中缓存或 service worker 的请求,会以从未接触 网络的条目形式出现。同一个 bug 的两次抓取经常互相矛盾。

文件的结构

log
├─ version "1.2" · creator{name,version} · browser
├─ pages[]     id, title, startedDateTime, pageTimings{onContentLoad,onLoad}
└─ entries[]   pageref, startedDateTime, time, connection, serverIPAddress
   ├─ request  method, url, httpVersion, headers[], cookies[],
   │           queryString[], postData{mimeType,text,params}
   ├─ response status, statusText, redirectURL, headers[], cookies[],
   │           content{size,compression,mimeType,text,encoding}
   ├─ cache    beforeRequest, afterRequest
   └─ timings  blocked, dns, connect, ssl, send, wait, receive

有两个细节最坑人。request.headers[]request.cookies[] 是同一批 cookie 的 两种独立表示,所以只遍历头的清洗会把整罐 cookie 原样留下。以 _ 开头的键 (_initiator_priority_resourceType_webSocketMessages)是厂商扩展: 在 Chrome 里有用,别处没有。而且几乎每个数值字段都可能是 -1——那是不可获得, 不是零——包括 bodySizepageTimings 和每一个计时阶段。

七个计时阶段

单位是毫秒,按发生顺序排列。

阶段量的是什么数值偏大通常意味着何时为 -1
blocked请求离开客户端之前的排队HTTP/1.1 每源约 6 条的上限;代理协商从未排队
dns主机名解析冷解析器、很长的 CNAME连接被复用
connectTCP 握手——包含 ssl源站遥远、RTT 高、无 keep-alive连接被复用
sslTLS 协商(计入 connect 内部)证书链庞大、没有会话恢复明文 HTTP,或连接复用
send把请求字节推出去上传体积很大
wait最后一个请求字节之后的 TTFB后端处理时间加一个 RTT
receive从线路上读取正文未压缩或体积过大的载荷

entry.time 是总耗时,实践中等于 blocked + dns + connect + send + wait + receive。由于 ssl 嵌套在 connect 里面,把七列全部相加就会把握手算两遍——这是 HAR 分析中最常见的算术错误。

重建瀑布图

两个坐标轴都能从 JSON 里恢复出来:水平位置是 entry.startedDateTime 减去所属 log.pages[].startedDateTime;宽度是 entry.time,再按上面那些阶段切开。 entry.connection 则显示哪些请求复用了同一个 socket。

接下来要读的是形状,不是数字。阶梯形——每个请求都在上一个结束时才开始——是依赖链, HTML → JS → API → 图片,任何服务端调优都治不了它,只能把资源发现提前。一片密集的 方块、并且越往下 blocked 段越长,那是客户端在排队;这也解释了为什么 HTTP/2 常常 能在没有任何一个响应变快的情况下把瀑布图压平。

选对工具

先把抓取结果粘进 HAR 文件查看器——它也在 SRE 工具精选 里。它把 log.entries[] 摊平成 Method / Status / Type / Size / Time / URL,附一行 N requests · X KB · Y ms total 汇总,并把 Status 单元格对 4xx/5xx 标红、 对 3xx 标橙:这是找出离群项最快的路径。只有元数据,从不含正文。

查看器给的是每条的总时间,不是阶段拆分。要看阶段,就用 JSONPath 查找器 直接查原始文件——$..timings 取出全部 拆分,或者用 $.log.entries[?(@.time>1000)].request.url 只取慢的那些。如果导出的 文件根本解析不了(下载被截断、粘贴时被折行),把它扔进 JSON 格式化,能拿到解析器报出的精确错误位置。

Status 那一列,HTTP 状态码参考 把一个裸数字变成名称、分类和 一行释义,可按码值或关键词过滤——分辨 400422 最快的办法。它止步于含义,不 涉及重定向行为:它的 301 那一行只说资源已永久移动。读一条重定向链时,这部分要从 协议本身补上——301302 允许客户端把 POST 改写成 GET 并丢掉正文,而 307308 不允许。完整对照是 HTTP 工具集 里的重定向矩阵,那里也 覆盖了响应头这一侧。手上没有抓取、只有一个 URL 时, HTTP 头信息检查工具 会从 api.sitekits.dev 在服务端取回它, 返回状态、重定向跳数和全部响应头——正文从不获取,URL 从不存储。用 JWT 令牌解码器 解开 401 里的 bearer token,看看 exp 是不是 已经过了;再用 REST API 测试器 重放一遍,它从你的浏览器 直连目标,因此 CORS 的表现跟在你自己的应用里一样。

落坑清单

一份原始 HAR 就等于一份凭证

会话 cookie、Authorization: Bearer …x-api-key、签名 URL、登录请求体,以及 你这个会话能读到的每一个响应,全都在里面。 HAR 文件清洗器 完全在你的浏览器里运行——什么都不上传——它把 敏感头、名字匹配 token|key|secret|password|passwd|pwd|auth|session|sig|signature 的参数,以及 整个 postData.text / content.text 的值替换成 [REDACTED],并报告一共命中了 多少处。即便浏览器给了你一个“已脱敏”的导出选项,也照样跑一遍——那些选项到底剥掉 什么,随版本而变。

模式脱敏不构成证明

头名是拿一份固定清单比对的,参数名是拿正则比对的,所以任何不按常规命名的东西都会 存活下来:藏在 URL 路径段里的密钥(/v1/reset/9f3c…)、结构化的 cookies[] 数组、serverIPAddress(你源站的真实 IP)、内部主机名。扫一遍输出,把任何可疑 URL 粘进 URL 解析器 看解码后的查询串,并在文件离开你的机器之前 grep 一遍 authorizationset-cookie。同一套条件反射也适用于 隐私工具安全工具精选

content.sizebodySize 量的是两回事

content.size 是解码后的正文长度;bodySize 是实际收到的字节数,省下了多少记在 content.compression 里。命中缓存的响应报告 bodySize: 0,所以用 content.size 汇总出来的 KB 数,会高估一个启用了压缩的站点的网络成本。

FAQ
HAR 里的总时间为什么跟我实测的页面加载时间对不上?
因为请求是重叠的。把 log.entries[] 里每条的 entry.time 相加,会把所有“同时有两个请求在飞”的毫秒重复计算一遍,所以这个和通常是墙上时钟时间的好几倍。要墙上时钟,请读 log.pages[].pageTimings.onLoad,或者取最早的 startedDateTime 到最晚的 startedDateTime 加上它自身 time 所构成的跨度。
HAR 条目里 wait 阶段很长,到底说明了什么?
wait 是从请求的最后一个字节发出之后开始计的首字节时间,所以它包含服务端自身的处理再加一个网络往返。wait 很粗而 receive 很细,指向后端——慢查询、冷缓存、跨区域跳转。反过来的形状,wait 很细而 receive 很粗,则说明响应本身就很大,或者链路很慢。
我已经把 HAR 里的 Cookie 头脱敏了,为什么会话 cookie 还在文件里?
因为 HAR 把 cookie 存了两遍。request.headers[] 里放的是原始的 Cookie 行,而 request.cookies[] 和 response.cookies[] 里放的是同一批值被解析成 name/value 对象后的形式,所以任何只遍历头的清洗都会把整罐 cookie 完好地留下——本站的 HAR 文件清洗器也是如此,它会处理头的值、查询参数、POST 参数以及两侧的正文,但不会碰 cookies[] 数组。把文件作为附件发出之前,先在里面搜一下 cookies,顺手也检查 serverIPAddress 和任何藏在 URL 路径段里的密钥。
为什么有些 HAR 条目的 dns 和 connect 是 -1?
因为这些阶段对那条请求根本没有发生。在 HAR 1.2 里,-1 表示该值不适用或不可获得,而这正是请求复用了既有 keep-alive 连接、因而不需要 DNS 查询、TCP 握手和 TLS 协商时得到的结果。把 -1 当成零无伤大雅;把它当作真实测量值参与求平均就不行了。
为什么我的 HAR 里没有请求体和响应体?
postData.text 和 content.text 都是可选字段,而 DevTools 会例行地省略过大或二进制的载荷,以保证导出文件还能用。正文存在时也可能是 Base64 而不是纯文本,此时 content.encoding 会被标记为 base64。正文缺失只说明导出方没有记录它,不代表响应是空的。