HTTP请求流程:为什么很多站点第二次打开速度会很快|浏览器篇
# HTTP请求流程:为什么很多站点第二次打开速度会很快
30 秒速记
HTTP建立在TCP连接之上,用于浏览器向服务器请求资源。- 浏览器通过
HTTP获取的资源可以包括HTML、CSS、JavaScript、图片和视频。 - 理解一次
HTTP请求,需要结合底层TCP的连接建立、数据传输和连接关闭三个阶段。 - 首次访问较慢、再次访问较快,以及再次进入网站仍保持登录状态,都与完整的
HTTP请求过程有关。 - 当前原文只是提出上述现象,没有给出缓存、身份状态保存等具体机制,因此不能从本段确定实现细节。
HTTP建立在连接之上,用来规定浏览器如何向服务器请求并获取页面资源。 一个页面通常不只是下载HTML,解析后还可能继续请求CSS、JavaScript、图片和视频,因此打开页面往往是一组有关联的网络活动。第二次访问更快可能与缓存或连接复用有关,保持登录则可能涉及Cookie和服务端会话。这里只能说明这些现象藏在完整请求流程中,不能仅凭现象认定网站采用了哪一种具体机制。
一个TCP连接过程包括了建立连接、传输数据和断开连接三个阶段。
而HTTP协议,正是建立在TCP连接基础之上的。HTTP是一种允许浏览器向服务器获取资源的协议,是Web的基础,通常由浏览器发起请求,用来获取不同类型的文件,例如HTML文件、CSS文件、JavaScript文件、图片、视频等。此外,HTTP也是浏览器使用最广的协议,所以要想学好浏览器,就要先深入了解HTTP。
不知道你是否有过下面这些疑问:
- 为什么通常在第一次访问一个站点时,打开速度很慢,当再次访问这个站点时,速度就很快了?
- 当登录过一个网站之后,下次再访问该站点,就已经处于登录状态了,这是怎么做到的呢?
这一切的秘密都隐藏在HTTP的请求过程中。所以,在今天这篇文章中,我将通过分析一个HTTP请求过程中每一步的状态来带你了解完整的HTTP请求过程,希望你看完这篇文章后,能够对HTTP协议有个全新的认识
原理拆解: HTTP定义浏览器与服务器交换资源的请求和响应语义,传统HTTP/1.1通常承载于TCP连接之上。浏览器发出请求后,服务器返回状态码、响应头和响应体;HTML解析过程中发现的CSS、JavaScript、图片等资源,又可能触发后续请求。因此,“打开一个页面”往往对应一组存在依赖关系的网络活动,而不是单个文件下载。
最小验证: 输入为同一路径的两次请求,验证服务器可以通过Cookie识别第二次访问。将代码保存为server.js并用node server.js运行,再连续执行两次所示的curl命令:
const http = require('node:http');
http.createServer((req, res) => {
const returning = req.headers.cookie?.includes('visited=1') ?? false;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Set-Cookie', 'visited=1; Path=/; HttpOnly');
res.end(returning ? 'returning visitor' : 'first visit');
}).listen(3000, () => {
console.log('http://localhost:3000');
});
curl -c cookies.txt http://localhost:3000
curl -b cookies.txt http://localhost:3000
第一条请求输出first visit并保存响应中的Cookie,第二条携带它后输出returning visitor。Set-Cookie负责写入状态,Cookie请求头负责回传;这只演示身份状态的一种基础机制,并不能推出所有网站都以这种方式维持登录。
边界与排查: 第二次访问更快可能涉及缓存、连接复用或其他环节,不能仅凭现象锁定原因;保持登录也可能依赖服务端会话及有效期策略。可在开发者工具的Network面板比较两次请求的状态、传输大小、时间分段和请求头,并在清除站点数据后复测。无痕模式、缓存禁用、凭据过期或服务器策略变化,都可能让第二次访问不再复用原有结果。
面试官追问
追问 1首页的HTML返回200后,页面仍没有样式、脚本和图片;产品认为一次HTTP请求成功就代表页面加载完成,你怎么解释?
一次成功响应只能说明对应资源已经取得,不能代表整个页面的网络活动完成。浏览器解析HTML时发现的CSS、JavaScript和图片等资源还可能触发后续请求;任一依赖资源失败都可能造成页面不完整。
追问 2同一用户连续两次访问站点,第二次明显更快;你会在浏览器里记录哪些证据,而不是直接归因于缓存?
应在Network面板比较两次请求的状态、传输大小、时间分段和请求头,并通过禁用缓存或清除站点数据做对照。第二次变快可能涉及缓存、连接复用或其他环节,仅凭总耗时无法锁定原因。
追问 3用户关闭页面后再次打开仍保持登录,评审者断言一定由localStorage实现,你如何用一个最小服务端实验反驳?
可让服务端首次响应发送Set-Cookie,客户端保存后在第二次请求通过Cookie头回传,服务端即可识别再次访问。该实验说明状态延续不必依赖localStorage;它只是基础机制示例,真实登录还可能依赖服务端会话和有效期策略。
追问 4测试人员在无痕窗口中复测“第二次访问更快且保持登录”,结果两种现象都消失了,应该怎样判断?
这说明原会话保存的缓存或凭据可能参与了结果,但仍不能仅凭无痕模式确定唯一机制。应分别检查资源缓存、请求头和身份状态,并在禁用缓存、清除站点数据及凭据过期等条件下对照;服务器策略变化也可能影响结果。
追问 5一个新闻页有HTML、CSS、JavaScript和多张图片,性能优化只允许先处理一类请求,你会依据什么排序?
应先从请求依赖关系和Network证据判断哪类资源阻塞页面或占用主要等待时间,不能因文件类型名称直接下结论。页面加载是一组相互依赖的请求,缓存与连接状态也会改变收益;缺少实际时序和传输数据时不宜承诺固定优先级。
追问 6服务端已经返回状态码和响应头,前端却只记录响应体内容;线上排查登录失效和缓存差异时会缺少什么?
会缺少解释服务器处理结果、缓存决策和状态传递的重要上下文。HTTP响应由状态码、响应头和响应体共同构成,登录状态还可能涉及Set-Cookie与后续Cookie;只保存响应体难以还原完整请求过程。
# 浏览器端发起HTTP请求流程
30 秒速记
- 浏览器先根据目标资源组织
HTTP请求行,确定请求方法、请求路径和协议版本。 - 发送前检查浏览器缓存;按题目描述,缓存命中时直接返回本地副本,不再访问源服务器。
- 缓存未命中后,从
URL获取主机与端口:域名经DNS解析为IP,未显式指定端口的HTTP请求默认使用80。 - 具备
IP和端口后,请求可能因同域连接数限制而排队;原文所述版本的Chrome对同一域名最多并发建立6个TCP连接。 - 获得连接名额后建立
TCP连接,再通过该连接传输HTTP请求行、请求头,以及POST等场景所需的请求体。 - 这条链路的主要耗时来自缓存未命中、
DNS查询、连接排队和TCP建连;缓存与DNS缓存可减少其中部分工作。
浏览器发起 HTTP 请求时,会依次构建请求、检查缓存、解析地址、准备连接并发送数据。 如果缓存副本可以直接使用,流程会在本地结束;否则浏览器通过 DNS 获取 IP,结合端口建立或复用 TCP 连接。连接可用后,请求行、请求头和可选的请求体才会发往服务器。实际排查慢请求时,我一般会重点看排队、DNS 和建连耗时;同域 6 条连接只是特定浏览器与时期的规则,不能套到 HTTP/2 等场景。
如果你在浏览器地址栏里键入地址:http://baidu.com, 那么接下来,浏览器会完成哪些动作呢?下面我们就一步一步详细“追踪”下
原理拆解: 浏览器先从URL确定协议、主机、端口和路径,并组织请求方法、请求目标及协议版本。真正访问网络前会执行缓存决策;若本地副本满足使用条件,可直接读取,否则继续准备连接。域名需要经DNS得到IP,未显式写端口的明文HTTP通常使用80。具备目标地址后,请求还可能等待连接名额,然后建立或复用TCP连接,最终发送请求行、请求头以及可选请求体。
最小验证: 输入为一个本地HTTP地址,验证客户端实际发送的请求方法、路径、请求头和请求体。将代码保存为request.js后执行node request.js:
const http = require('node:http');
const server = http.createServer((req, res) => {
let body = '';
req.setEncoding('utf8');
req.on('data', chunk => body += chunk);
req.on('end', () => {
console.log({ method: req.method, url: req.url, body });
res.end('ok');
});
});
server.listen(3000, '127.0.0.1', () => {
const req = http.request({
hostname: '127.0.0.1',
port: 3000,
path: '/search?q=tcp',
method: 'POST',
headers: { 'Content-Type': 'text/plain' }
}, res => {
res.setEncoding('utf8');
res.on('data', data => console.log(data));
res.on('end', () => server.close());
});
req.end('browser request');
});
服务端预期打印POST、/search?q=tcp和browser request,客户端打印ok。http.request根据主机和端口建立连接,path进入请求行,headers与req.end的参数分别形成请求头和请求体。若端口被占用、地址不可达或连接被拒绝,程序会触发错误;生产代码应监听请求和服务器的error事件。
边界与排查: “缓存命中就完全不访问服务器”只适用于可直接使用本地副本的情况;需要重新验证的缓存仍可能发起条件请求。原文提到的同域最多6条连接属于特定时期、特定浏览器及协议实现,不能当作所有版本的固定规则,HTTP/2等协议的连接与并发模型也不同。排查耗时时,可在开发者工具中区分排队、DNS、连接建立、请求发送和等待响应,并通过禁用缓存、清空DNS状态或切换全新会话做对照实验。
