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——那是不可获得, 不是零——包括 bodySize、pageTimings 和每一个计时阶段。

七个计时阶段

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

阶段量的是什么数值偏大通常意味着何时为 -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 状态码参考 把一个裸数字变成名称、分类和 一行释义,可按码值或关键词过滤——分辨 400 与 422 最快的办法。它止步于含义,不 涉及重定向行为:它的 301 那一行只说资源已永久移动。读一条重定向链时,这部分要从 协议本身补上——301 和 302 允许客户端把 POST 改写成 GET 并丢掉正文,而 307 和 308 不允许。完整对照是 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 一遍 authorization 和 set-cookie。同一套条件反射也适用于 隐私工具 和 安全工具精选。

content.size 和 bodySize 量的是两回事

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。正文缺失只说明导出方没有记录它,不代表响应是空的。