浏览器渲染原理
页面从输入一个URL到渲染的过程
- DNS解析
- 建立tcp链接(tcp的三次握手建立一个链接)
- 发送http请求
- 服务器收到请求之后处理并返回页面
- 浏览器接收到响应之后开始渲染页面
- 解析HTML生成DOM树
- 解析CSS生成CSSOM树
- DOM树+CSSOM树生成Render树
- 根据Render树开始渲染页面(涉及到重绘和重排)
- TCP 四次挥手断开链接
info
大致过程: ● DNS解析 ● 建立TCP链接 ● 客户端发送请求 ● 服务器处理和响应请求 ● 浏览器解析并渲染内容 ● TCP四次挥手断开链接
info
TCP 三次握手
C -> S SYN + Seq
S -> C ACK = Seq + 1 & SYN & Seq
C -> S ACK = Seq + 1
链接建立成功
TCP 四次挥手
C -> S FIN + Seq
S -> C ACK = Seq + 1
S -> C FIN + Seq
C -> S ACK = Seq+1
服务器收到客户端的确认之后会立即断开链接,但是客户端在发送ACK之后会等待2MSL然后在关闭链接,原因:
保证客户端发送的最后一个ACK报文能够到达服务器。因为这个ACK报文可能会丢失,站在服务器的角度我已经发送了ACK+FIN报文请求断开了,客户端没有给我回应,应该是我发送的FIN+ACK报文客户端没有收到,那么我就会重新发送。那么这个客户端就可能在这个2MSL时间内收到这个重传的报文,然后重启2MSL定时器
客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
浏览器接收到 HTML 文件并转换为 DOM 树
在解析 HTML 文件的时候,浏览器还会遇到 CSS 和 JS 文件,这时候浏览器也会去下载并解析这些文件。
将 CSS 文件转换为 CSSOM 树
生成渲染树
渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。
为什么操作 DOM 慢
因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。
经典面试题:插入几万个 DOM,如何实现页面不卡顿?
大部分人应该可以想到通过 requestAnimationFrame 的方式去循环的插入 DOM,其实还有种方式去解决这个问题:虚拟滚动(virtualized scroller)。这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。
什么情况阻塞渲染
首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。
JS 线程和渲染线程是互斥的,原因是JS可以操作DOM,所以为了避免页面展示的不一致性,将两个线程设置为互斥的。
所以在解析执行JS代码的时候会阻塞渲染。
js执行会阻塞DOM树的解析和渲染
info
css加载会阻塞DOM树的解析和渲染吗?
1、css并不会阻塞DOM树的解析 2、css加载会阻塞DOM树渲染 3、css加载会阻塞后面js语句的执行
(由于 js 可能会操作之前的 Dom 节点和 css 样式,因此浏览器会维持html中css和js的顺序。因此,样式表会在后面的js执行前先加载执行完毕。所以css会阻塞后面js的执行。)
(同步)js脚本执行会阻塞其后的DOM解析(所以通常会把css放在头部,js放在body尾)。 p.s. 不严谨地说,某些情况下css会因为js脚本间接阻塞DOM解析
async还是会阻塞渲染的 async与defer一样 下载不阻塞html解析 但async是js谁下载完谁就立即执行,会阻塞html解析,defer是html解析完成后才按顺序执行下载的js
然后当浏览器在解析到 script 标签时,会暂停构建 DOM,加载解析并执行JS完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
可以给 script 标签添加 defer 或者 async 属性来避免JS阻塞DOM元素的渲染。
当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。
对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载解析完成之后立即执行不会阻塞渲染。
重绘(Repaint)和回流(Reflow)
- 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
- 回流是布局或者几何属性需要改变就称为回流。
回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
引起回流的操作
- resize窗口
- 页面第一次渲染
- 修改字体大小
- 修改元素的尺寸或者位置
- 元素的内容发生变化
- 添加或者删除可见的DOM元素
- 访问以下属性或者是调用下面的方法
- with、height
- offesetWidth、offsetHeight、offsetLeft、offsetTop
- clientWidth、clientHeight、clientLeft、clientTop
- scrollWidth、scrollHeight、scrollLeft、scrollTop
- getCompputedStyle()
- scrollIntoview()、scrollIntoViewIfNeeded()
- scrollTo()
- getBoundingClientRect()
引重绘的操作
元素的大小和位置没有发生变化,仅仅是外观改变(color、backgroundColor、visibility等)
性能影响
回流比重绘的代价更高
有时仅仅回流了一个单个的元素,其父元素以及跟随者它的元素也会发生回流。
现代的浏览器对回流和重绘都进行了优化。
浏览器会维护一个队列将所有引起回流和重绘的操作都放入这个队列中,如果这个队列中的任务数量或者时间间隔到达一个阈值,浏览器就会清空队列,进行一次批处理。这样就可以把多次的回流和重绘变为一次。
当你访问以下属性或者调用下面的方法的时候浏览器会立即清空队列:
● clientWidth/Heigth/Top/Left ● scrollWidth/Height/Top/Left ● offsetTop/Left/Width/Height ● getComputedStyle() ● getBoundingClientRect() ● width、height
因为队列中可能会有影响这些属性或者方法返回值的操作,所以浏览器会立即清空队列,确保你拿到的值是最精确的。
info
- 当一轮事件循环结束后,会去更新DOM,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。
- 然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
- 判断是否触发了 media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行 requestAnimationFrame 回调
- 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
- 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。
如何避免
CSS
● 避免使用table布局 ● 避免使用calc表达式 ● 尽可能改变DOM树最末端的class ● 将动画应用在position为absolute/fixed的元素上
JS
● 避免频繁的操作样式,最好是可以一次性重写style或者将所需要修改的样式添加到一个class上,并一次性修改class ● 避免频繁操作DOM,可以创建一个documentFragment,然后在它上面进行DOM操作,最后把它添加到文档中 ● 可以将元素设置为display: none;操作结束之后再将它显示出来。 ● 避免频繁的读取会引发回流的属性或者调用会引发回流的方法,可以使用一个变量将其返回值缓存起来 ● 对具有复杂动画的元素设置position为absolute/fixed使它脱离文档流,否则会引起父元素及其后续元素频繁回流
当发生 DOMContentLoaded 事件后,就会生成渲染树,生成渲染树就可以进行渲染了,这一过程更大程度上和硬件有关系了。