一、从url到页面渲染的全过程
- 浏览器接收到url之后,开启一个进程,解析url,提取url的协议、域名、路径等信息。
- 浏览器会向本地的DNS服务器发起请求,DNS服务器通过多层查询将该域名解析为对应的ip地址,浏览器会向该ip地址所在的服务器发起请求,建立TCP连接并交换数据。
- 服务器接收到浏览器的请求并处理数据,返回:状态码、响应头、响应体。
- 浏览器通过状态码判断请求是否成功,如果成功,则进行页面渲染。(此处会将html解析成DOM树,css解析为CSSOM树(样式树),合并生成render渲染树。然后会进行layout布局,此处可能发生回流或者重绘。最后将页面渲染出来。)
- 连接结束,断开TCP连接。(四次挥手)
二、浏览器渲染过程中遇到JS文件如何处理
JS文件会阻止html的解析,只有下载并执行完JS脚本才会继续解析html文件。
其中有两个defer和async标签。
defer和async的共同点:
- defer和async在下载js脚本的时候,都不会阻止html文件的解析。(下载此类脚本都不会阻止页面呈现(异步加载))
defer和async的不同点:
- defer在解析html的过程中,遇到js文件解析,会同步解析html文件,等html文件解析完成后,才会执行js文件。
- async在解析html的过程中,遇到js文件解析,会同步解析html文件,在js文件解析完成后,会暂停html文件的解析,立刻执行js文件,执行完成后会继续html文件的解析。
三、页面渲染的流程
- 将html文件解析成DOM树。
- 遇到非阻塞资源,如css文件、图片等,不会阻塞html的解析。
- 遇到阻塞资源,如js文件,会同步解析html文件,在js脚本执行完成后,继续解析html。
- 将css文件解析成CSSOM树。
- 将DOM树和CSSOM树合并生成renderTree。
- 布局(layout):计算节点在屏幕的大小和位置。
- 重排(回流):如果节点在屏幕的大小和位置发生改变,则会发生重排,重新计算节点在屏幕的大小和位置。
- 重绘:如果节点发生除了大小和位置的变化,如颜色,则会发生重绘。
- 浏览器将renderTree渲染到屏幕上,页面解析结束。
四、解析URL
URL,统一资源定位符。
- 协议:指资源访问的协议,常见的协议包含HTTP、HTTPS、FTP。
- 主机号:指服务器的域名或者IP地址,用于唯一标识一个服务器。
- 端口号:服务器上提供服务的端口号,可以忽略。
- 路径:服务器上的资源路径,指访问资源时需要进入的目录层级及资源名称。
- 查询参数:资源请求参数,格式为key=value,多个参数使用&进行连接。
- 锚点:指#后面的hash值,用于定位到某个地址。
五、DNS域名解析
- DNS的作用:将域名解析成IP地址。
- IP和域名的关系:IP地址是网络设备的标识符,域名是方便人们记忆和使用的网络地址别名。
- DNS域名解析流程:
- 首先浏览器在本地缓存中查询,如果有该域名对应的IP地址,直接返回,解析结束。
- 如果浏览器缓存中没有查找到该域名对应的IP地址,则向本地的DNS服务器发起查询请求,如果找到直接返回,解析结束。
- 如果本地的DNS服务器中没有找到该域名对应的IP地址,则向根域名服务器发起查询请求,根域名服务器会返回一个顶级域名服务器地址,本地的DNS服务器向顶级域名服务器发起查询请求,顶级域名服务器会返回一个权威DNS服务器地址,本地DNS服务器向权威DNS服务器发起查询请求,获取到该域名对应的IP地址,本地DNS服务器将其保存在缓存中,并将结果返回给浏览器,浏览器将结果缓存后,并使用该IP地址访问对应的网站。
六、建立TCP连接
- 三次握手:
客户端:hello,你是server吗?
服务端:hello,我是server,你是client吗?
客户端:是的,我是client。
- 第一次握手:客户端发起一个同步标志位(SYN)给服务端。
- 第二次握手:服务端接收后,发送一个同步标志位(SYN)及确认标志位(ACK)给客户端。
- 第三次握手:客户端接收后,发送一个确认标志位(ACK)给服务端,握手结束。
- 四次挥手:
客户端:拜拜
服务端:先别走,你的东西没拿完
服务端:好了,可以了
客户端:拜拜
- 客户端:客户端发送一个结束标志位(FIN)给服务端,进入等待结束(FIN_WAIT)状态。
- 服务端:服务端接收后,发送一个确认标志位(ACK)给客户端,此时服务端进入等待关闭(CLOSE_WAIT)状态。
- 服务端:服务端发送一个结束标志位(FIN)给客户端,此时服务端进入最后确认关闭(LAST_ACK)状态。
- 客户端:客户端接收到结束标志位(FIN),发送一个确认标志位(ACK)给服务端,服务端接收确认后,进入关闭(CLOSE)状态,完成四次挥手。
- 为什么是三次握手,不是两次或者四次:
- 两次握手的话,客户端和服务端无法相互确定对方身份。
- 四次握手的话,双方确认身份后,浪费了一次资源。
七、浏览器的缓存策略
浏览器的缓存策略分为两种:强缓存和协商缓存。
- 强缓存:使用强缓存策略时,如果缓存资源在过期时间内,浏览器可以直接从本地缓存中读取资源。
- 协商缓存:如果强缓存失效后,浏览器会携带上一次请求响应体中的缓存标识向服务器发起请求,服务器判断该资源是否更新,如果未更新,则返回304状态码,告诉浏览器资源未被更新,可以使用本地缓存,否则返回新的资源内容。
浏览器的缓存策略:
- 浏览器每次发起请求,都会从浏览器缓存中查找该请求结果以及缓存标识。
- 浏览器每次拿到请求结果时,都会把请求结果和缓存标识存在浏览器缓存中。
八、状态码
-
200: 请求成功,从客户端发送给服务端的请求正常处理并返回数据。
-
204: 没有返回内容。(no content)
-
301: 被请求的资源永久的移动到新的地址。(永久重定向)
-
302: 被请求的资源临时的移动到新的地址。(临时重定向)
-
304: 表示服务器的资源未被修改,通常是客户端发起一个请求,服务端通过对比资源修改时间来确定该资源是否被修改。
-
400: 请求出错。(bad request)
-
401: 请求权限未验证。
-
403: 没有权限。
-
404: 被访问的资源不存在。(not found)
-
405: 不允许的HTTP请求方法,意味着正在使用的HTTP请求方法不被服务器允许。
-
500: 服务器内部错误。
-
503: 服务器无法处理该请求。
九、跨域
跨域:跨域基于浏览器的同源策略。
同源策略:协议名、主机号、端口号完全一致。
同源策略作用:同源策略是浏览器为了保护用户数据安全和服务器安全而实行的一种安全机制,防止网站通过恶意脚本访问其他网站的数据。
场景:当客户端(浏览器)向不同源的服务器发起请求时,浏览器会拦截响应,防止跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等安全问题。
解决跨域问题
- CORS(跨域资源共享)
- Nginx反向代理
- Node.js中间件代理
- WebSocket
- postMessage
十、浏览器的垃圾回收机制
浏览器会自动回收不再使用的内存的机制,避免内存泄漏。
触发条件:
- 对象没有引用
let a = {} a = null;原对象没有引用,可以回收
- 作用域结束
- 函数执行完,局部变量引用结束,可回收
- DOM节点被移除且没有js引用
div.remove()并且没有变量引用该div,可回收
常用的GC算法:
- 引用计数法
- 标记清除法
- 分代回收
- 增量标记/并行回收
十一、浏览器的并发控制
原理
浏览器对同一域名下的HTTP请求数量有限制,就是说,浏览器不会无限同时发请求。
##3 浏览器的并发数限制是多少 HTTP/1.1: 同一域名下并发数量通常为6个,不同浏览器略有差异(一般6~8)
HTTP/2:使用多路复用,在一个TCP连接里同时传输多个请求,不再受6个限制。
浏览器为什么限制6个
- 避免TCP连接过多
- 防止带宽滥用
- 控制资源消耗
HTTP/2为什么可以突破限制
- 多路复用
- 一个TCP连接内并发多流
- 避免队头阻塞
为什么要做前端并发控制
- 防止接口压垮后端
- 避免浏览器资源占用过高
- 大文件分片上传
前端如何实现并发控制
- 请求队列控制
- 维护一个执行中的任务数
- 超过最大并发数就排队
- 完成一个在执行下一个
class RequestQueue {
constructor(max) {
this.max = max
this.running = 0
this.queue = []
}
add(task) {
return new Promise((resolve) => {
this.queue.push(() => task().then(resolve))
this.run()
})
}
run() {
if (this.running >= this.max || !this.queue.length) return
this.running++
const task = this.queue.shift()
task().finally(() => {
this.running--
this.run()
})
}
}
- Promise.all + 切片执行
async function runTasks(tasks, limit) {
const result = []
for (let i = 0; i < tasks.length; i += limit) {
const slice = tasks.slice(i, i + limit)
const res = await Promise.all(slice.map(fn => fn()))
result.push(...res)
}
return result
}
- 使用现成的库
- p-limit
- async.js
- 结合你的项目说一下
- 大文件上传
- 分片上传的时候做了并发控制,比如限制4个切片同时上传,避免瞬间大量请求占满带宽或者压垮后端
- 无痛刷新token
- 在token刷新时也做了并发控制,通过队列机制保证只触发一次refresh,其余请求挂起等待,避免重复刷新。
如果用户进入页面加载慢,该怎么处理
当用户反馈页面加载慢时,我会先进行排查,确认问题范围,从【网络层 - 资源层 - 执行层 - 渲染层 - 接口层】分层排查,通过NetWork面板查看请求耗时和资源体积等,通过Performance面板查看是否存在主线程阻塞或长任务。如果是打包体积问题,使用bundle工具看是否需要做代码分割或者按需加载,如果是渲染问题,考虑虚拟列表,如果是接口慢,协调后端优化
确认问题范围
-
确认问题范围
- 是所有用户慢,还是个别用户
- 首次加载慢,还是每次进入都慢
- 是弱网络环境还是正常网络
判断是环境问题还是系统问题
排查
-
NetWork面板排查
- 请求耗时
- TTFB是否过长(后端慢),在Network面板里,点击任意请求,Timing标签下,Waiting(TTFB)。
- Content Download是否慢(资源大),在Network面板里,点击任意请求,Timing标签下,Content Download。
- Stalled是否很长(浏览器并发限制),在Network面板里,点击任意请求,Timing标签下,资源调度。
- 资源体积
- js是否过大
- 图片是否未压缩
- 是否开启gzip/br
- 请求是否串行(请求A完成 - 请求B发起 - 再发起请求C)
- 请求耗时
-
Performance面板排查
- 录制页面看加载过程,
- 是否存在Long Task (>50ms)
- JS执行时间是否过长
- 是否大量回流/重绘(reflow/repaint)
- 是否有同步阻塞代码
- 录制页面看加载过程,
-
打包体积分析
使用webpack-bundle-analyzer
- 是否第三方库过大
- 是否没做按需加载
解决
- 路由懒加载
- 组件按需引入
- 动态import
-
渲染层分析
如果页面DOM很多:
- 大表格是否没做虚拟滚动
- 是否一次渲染上千条数据
- 是否存在重复渲染
解决
- 虚拟列表
- 分页
- 懒加载
-
接口层分析
如果TTFB长,
解决
- loading优化
- 骨架屏