内容总结自 浏览器工作原理 这门课程,将重点的有收获的内容总结如下,同时发现了一个不花钱的镜像网站 here,这个文章非常不错的,适合多次阅读。
一、从输入url到展示出来页面经历过什么?
-
浏览器对输入内容进行判断
- 如果是url,则发起网络请求
- 否则,使用浏览器默认的搜索引擎拼接搜索结果
-
浏览器发起网络请求,查看本地缓存是否有,有的话直接返回,没有则向服务器发起请求
-
进行dns解析
- 检查浏览器缓存
- 检查操作系统缓存,如host文件
- 检查路由器缓存
- 如果都没有,向DNS服务器查询
- 根服务器返回顶级域名服务器地址
- 顶级服务器返回次级服务器地址
- 次级服务器通过域名查找到ip
- local dns server缓存结果,并返回给用户,缓存在系统中
-
和服务器创建tcp链接,如果是https还要建立tls链接
-
构造请求头、请求行等信息,把域名相关cookie附加到请求头中,发送请求信息
-
服务器返回相应数据
-
浏览器接受到数据后,解析响应头,
- 如果状态码是301(永久重定向),302(临时),则重定向到其他url,从location字段里读取重定向地址,发起新的http或者https请求,一切重头开始
- 如果是200,则进行相应数据处理,根据content-type字段区分是什么类型,如果是text/html,则进行html解析
-
准备渲染进程:如果是同一站点的网页共用渲染进程,否则新建渲染进程
-
渲染进程
- 构建dom树:解析html构造dom树:html是边下载边解析
- 样式计算:解析css构造cssom树
- css构造会阻塞下面的布局树生成,因此适合放在头部,css的下载不会阻塞dom树解析
- js如果在头部会阻塞dom的解析,js下载和执行都会阻塞,所以js要放在底部,而且js可能会操作dom,所以也是要放到底部的
- css可能会阻塞dom渲染:当头部有js脚本的时候,css会阻塞dom树构建。因为js脚本执行之前是需要把它之前的css编译执行完的,因为js可能也会修改css,所以头部如果有脚本,会间接造成css对dom解析的阻塞,css在这种情况下也会阻塞dom渲染。
- 布局:根据dom树和cssom树生成布局树,包含元素位置信息,不含display none、head、script标签,同时计算样式,为每个节点找到对应的样式,并且需要计算出来每个元素的位置。
- 分层:对布局树进行分层,生成分层树,是为了z-index,position,滚动条等复杂的分层操作。
- 绘制:为每个分层生成绘制列表,提交到合成线程
- 光栅化:合成线程将图层分成图块,光栅化线程中转成位图,分块是为了让渲染一块一块出来,这样节省性能。
- 合成:合成线程发送绘制图块命令到浏览器线程
- 显示:浏览器线程生成页面
二、性能优化方式
网络方面
-
减少请求个数
- js和css内敛
- js如果不需要操作dom,则改成async或者defer操作
- css添加媒体查询减少下载量
- css放在头部,js放在底部或者异步加载
-
减小资源的大小
- 压缩css和js,移除注释内容
-
减少请求时间
- 静态资源放cdn
- 使用http2
-
图片优化
- css代替图画
- 图片裁剪
- 小图片打包成base64
- 使用字体图标
- 选择争取的图片格式,webp等,svg等
-
webpack
- 压缩代码
- cdn加速
- tree shaking
- code split:按需加载
- splitChunksPlugin来提取公共模块,利用浏览器缓存无需长期变动的公共代码
渲染流程
-
减少重绘与重排
- js减少对样式的操作,能用css用css
- 避免逐条改变样式,用类名改变样式
- dom尽量少操作,用
createDocumentFragment完成整块dom操作后再添加到文档中 - dom离线,先设置成none,设置好后再现实出来
- 避免同步布局,不要在操作dom之后立马查询dom的属性,这样会导致同步布局,因为正常的流程是js一个task,之后样式重新布局会到另一个task中,如果立刻读取的话,需要马上布局,造成同步布局,影响性能。
- 利用节流和防抖
- 利用css合成动画,可以将有动画的元素添加will-change属性,是 告诉渲染引擎需要将该元素单独生成一个图层
- 动画的元素设置postion是absulte或者fixed
- 不要使用table布局,改动一个属性可能会使table整个布局
-
减少js脚本执行时间
- 将任务拆解,分成多个小任务
- web worker
-
懒加载
三、浏览器垃圾回收策略
-
栈内存回收:通过下移ESP来销毁函数保存在栈中的执行上下文。如果有新的函数调用的话会直接覆盖这段新的内容。
-
堆内存回收
-
垃圾回收的基础:代际假说
- 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
- 第二个是不死的对象,会活得更久。
-
V8垃圾回收
- 把堆分为新生代和老生代两个区域,新生代存放的是生存时间短的对象,老生代存放的是生存时间久的对象。新生代通常只支持1-8m的容量。
- 新生代垃圾回收:副垃圾回收器(Scavenge算法,划分新老区域)
- 老生代垃圾回收:主垃圾回收器
-
垃圾回收算法公共流程
-
- 标记空间中活动对象和非活动对象
-
- 回收非活动对象所占据的内存
-
- 内存整理:频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的
-
-
副垃圾回收器(新生代-Scavenge算法)
- Scavenge算法:将新生代空间分为两个区域:对象区域、空闲区域。新加入的对象存入对象区域,当对象区域写满时,需要执行一次垃圾清理操作。
- 垃圾回收中,对对象区域垃圾做标记,标记完进入清理阶段,副垃圾回收器把存活的对象复制到空闲区域中,同时有序的排列起来。
- 复制完成后,对对象区域与空闲区域进行角色翻转,将原来的对象区域变成空闲区域,原来的空闲区域变成对象区域。角色反转让新生代的这两块区域无限重复使用下去。
- 对象晋升策略:经过两次垃圾回收依然存活的对象,会被移动到老生区中
-
主垃圾回收器:标记-整理算法
-
大的对象直接分配到老生区,对象占用空间大,对象存活时间长。
-
标记阶段:从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
-
整理阶段:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
-
增量标记算法:为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成。
-
-
四、浏览器的组成,包含哪几个部分
- 浏览器进程:负责界面展示、用户交互、子进程管理,同时提供存储功能
- 渲染进程:包含浏览器内核和js引擎
- 浏览器内核:排版引擎blink
- js引擎v8
- GPU进程
- 网络进程:负责页面的网络资源加载
- 插件进程
五、http1、http2、http3
http1
http1.0
最重要是是引入了请求头和响应头,支持多种类型文件传输。
- 引入了请求头和响应头
// 期望返回的服务类型
accept: text/html
// 期望服务器采用的压缩方式
accept-encoding: gzip, deflate, br
// 期望返回的文件编码
accept-Charset: ISO-8859-1,utf-8
// 期望页面语言:中文
accept-language: zh-CN,zh
-
引入了响应头状态码
-
提供了cache机制,缓存已下载过的数据
-
加入了用户代理
http1.1
最重要的是引入了持久链接keep-alive
- keep-alive:HTTP/1.1 中增加了持久连接的方法,它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持。目前浏览器中对于同一个域名,默认允许同时建立 6 个 TCP 持久连接。
- http对头阻塞:持久连接虽然能减少 TCP 的建立和断开次数,但是它需要等待前面的请求返回之后,才能进行下一次请求。如果 TCP 通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后面的所有请求。
- 虚拟主机的支持:HTTP/1.1 的请求头中增加了 Host 字段,用来表示当前的域名地址,这样服务器就可以根据不同的 Host 值做不同的处理。
- 动态内容的支持:HTTP/1.1 通过引入 Chunk transfer 机制来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。这样就提供了对动态内容的支持。
- cookie支持
http2
http1.1的问题
-
TCP的慢启动:一旦一个 TCP 连接建立之后,就进入了发送数据状态,刚开始 TCP 协议会采用一个非常慢的速度去发送数据,然后慢慢加快发送数据的速度,直到发送数据的速度达到一个理想状态,我们把这个过程称为慢启动。
-
多条TCP连接,会竞争固定的宽带
-
HTTP1.1队头阻塞问题
http2的优化
HTTP/2 的思路就是一个域名只使用一个 TCP 长连接来传输数据,这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个 TCP 连接竞争带宽所带来的问题。
多路复用是通过在协议栈中添加二进制分帧层来实现的,有了二进制分帧层还能够实现请求的优先级、服务器推送、头部压缩等特性,从而大大提升了文件传输效率。
- 多路复用:添加了一个二进制分帧层,可以将请求分成一帧一帧的数据去传输,这样带来了一个额外的好处,就是当收到一个优先级高的请求时,比如接收到 JavaScript 或者 CSS 关键资源的请求,服务器可以暂停之前的请求来优先处理关键资源的请求。
- 设置请求的优先级
- 服务器推送:HTTP/2 还可以直接将数据提前推送到浏览器。服务器知道该 HTML 页面会引用几个重要的 JavaScript 文件和 CSS 文件,那么在接收到 HTML 请求之后,附带将要使用的 CSS 文件和 JavaScript 文件一并发送给浏览器。
- 头部压缩
http3
http2问题
- TCP队头阻塞:如果在数据传输的过程中,有一个数据因为网络故障或者其他原因而丢包了,那么整个 TCP 的连接就会处于暂停状态,需要等待丢失的数据包被重新传输过来。
在 HTTP/2 中,多个请求是跑在一个 TCP 管道中的,如果其中任意一路数据流中出现了丢包的情况,那么就会阻塞该 TCP 连接中的所有请求。这不同于 HTTP/1.1,使用 HTTP/1.1 时,浏览器为每个域名开启了 6 个 TCP 连接,如果其中的 1 个 TCP 连接发生了队头阻塞,那么其他的 5 个连接依然可以继续传输数据。
当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。
-
TCP建立连接的延时:TCP建立连接需要1.5个RTT,如果要建立TLS链接,大概需要2-3个RTT,所以在传输数据之前,需要花费3-4个RTT。
-
TCP协议僵化:中间设备的僵化、操作系统僵化。如果我们在客户端升级了 TCP 协议,但是当新协议的数据包经过这些中间设备时,它们可能不理解包的内容,于是这些数据就会被丢弃掉。这就是中间设备僵化,它是阻碍 TCP 更新的一大障碍。
QUIC(http3)
HTTP/3 选择了一个折中的方法——UDP 协议,基于 UDP 实现了类似于 TCP 的多路数据流、传输可靠性等功能,我们把这套功能称为 QUIC 协议。
- 实现了类似TCP的流量控制、传输可靠性的功能。
- 集成了TLS加密功能:使用了TLS1.3
- 实现了http2的多路复用功能。
- 实现了快速握手的功能:因为是基于UDP的,所以QUIC可以实现使用0RTT或者1RTT来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。
http3挑战
- 服务器和浏览器没有对http3提供比较完整的支持。
- 部署http3存在问题,因为系统内核对UDP的优化远远没有达到TCP的优化程度。
- 中间设备僵化问题。
六、浏览器安全
七、webpack优化
如何提高打包速度
- 优化loader:babel-loader排除node_modules,用include锁定编译范围,开启缓存
loader: 'babel-loader?cacheDirectory=true' - happypack:webpack正常打包是单线程的,加上happyPack可以使loader变为并行执行
- DllPlugin:将特定的类库提前打包然后引入,一般用于类库打包
- 代码压缩
- resolve.alias: 别名方式映射路径,更快找到文件
如何减少webpack打包体积
- 按需加载
- 作用域提升
- tree shaking