先从一道经典的面试题说起:从浏览器地址栏输入 url 到请求返回发生了什么?
- 浏览器根据 DNS 服务器得到域名的 IP 地址
- 向这个 IP 的机器发送 HTTP 请求
- 服务器收到、处理并返回 HTTP 请求
- 浏览器接收到服务器返回的内容
返回的内容如下:其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。
- 解析HTML,生成DOM树,解析CSS,生成CSSOM树
- 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
- Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
- Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
- Display: 将像素发送给GPU,最后通过调用操作系统Native GUI的API绘制,展示在页面上。
如果觉得抽象可以借助以下例子理解:
- 浏览器渲染过程 = 盖房子的整个流程
- 解析 HTML(打地基):从 HTML 中构建 DOM 树。
- 解析 CSS(装饰蓝图):从 CSS 中构建 CSSOM 树。
- 合成 Render Tree(房间设计图):将 DOM 和 CSSOM 结合,生成 Render Tree。
- 布局(安排家具):确定每个元素的具体位置和大小。
- 绘制(粉刷墙壁):根据 Render Tree 绘制页面的像素。
- 合成和显示(最终交付):将图层合成并渲染到屏幕。
note:
1、无论通过什么方式影响了元素的几何信息(元素在视口内的位置和尺寸大小),浏览器需要重新计算元素在视口内的几何属性,这个过程叫做回流,也叫重排。
2、所谓重绘就是:通过构造渲染树和重排(回流)阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(元素在视口内的位置和尺寸大小),接下来就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘。
3、如何减少重排和重绘?
- 最小化重绘和重排,比如样式集中改变,使用添加新样式类名 .class 或 cssText 。
- 批量操作 DOM,比如读取某元素 offsetWidth 属性存到一个临时变量,再去使用,而不是频繁使用这个计算属性;又比如利用 document.createDocumentFragment() 来添加要被添加的节点,处理完之后再插入到实际 DOM 中。
- 使用 absolute 或 fixed 使元素脱离文档流,这在制作复杂的动画时对性能的影响比较明显。
- 开启 GPU 加速,利用 css 属性 transform 、will-change 等,比如改变元素位置,我们使用 translate 会比使用绝对定位改变其 left 、top 等来的高效,因为它不会触发重排或重绘,transform 使浏览器为元素创建⼀个 GPU 图层,这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。
- 使用 CSS 动画代替 JavaScript 动画
- Lazy Load(懒加载)和 Virtual List(虚拟列表):对于大数据渲染,按需加载数据或只渲染可见部分。
在渲染的过程中,遇到JS怎么办?
渲染过程中,如果遇到script就停止渲染,执行 JS 代码。因为浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
为什么呢?原因在于JS代码可能会改变DOM的结构(比如执行document.write()
等API)
也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer(延迟) 或者 async(异步) 属性。
浏览器架构
不同的浏览器使用不同的架构,下面主要以Chrome为例,介绍浏览器的多进程架构。
在Chrome中,主要的进程有4个:
- 浏览器主进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。
- 渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。
- 插件进程 (Plugin Process):负责控制网页使用到的插件
- GPU进程 (GPU Process):负责处理整个应用程序的GPU任务
这4个进程之间的关系是什么呢?它们是如何分工协作的呢?
- 首先,当我们是要浏览一个网页,我们会在浏览器的地址栏里输入URL,这个时候
Browser Process
会向这个URL发送请求,获取这个URL的HTML内容, - 然后将HTML交给
Renderer Process
,Renderer Process
解析HTML内容,解析遇到需要请求网络的资源又返回来交给Browser Process
进行加载,同时通知Browser Process
,需要Plugin Process
加载插件资源,执行插件代码 - 解析完成后,
Renderer Process
计算得到图像帧,并将这些图像帧交给GPU Process
,GPU Process
将其转化为图像显示屏幕。(将像素发送给GPU,最后通过调用操作系统Native GUI的API绘制,展示在页面上)
为什么需要 defer
和 async
为了解决传统 <script>
的阻塞问题,提升页面加载性能,HTML 引入了 defer
和 async
属性。
异步加载和直接加载有何区别
- script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
- async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。
- defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本
共同点:都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析
属性 | 加载方式 | HTML 解析 | 执行时间 | 执行顺序 |
---|---|---|---|---|
无属性 | 阻塞加载 | 暂停,直到脚本加载和执行完成 | 立即执行,阻塞 HTML 解析 | 按照书写顺序执行 |
defer | 异步加载(并行加载) | 与 HTML 解析同时进行 | HTML 解析完成后执行 | 按照书写顺序执行 |
async | 异步加载(并行加载) | 与 HTML 解析同时进行 | 脚本加载完成后立即执行(可能打断 HTML 解析) | 加载完成即执行,顺序不确定 |
defer
的使用场景:
- 页面主要逻辑的 JavaScript 文件。
- 脚本需要等待完整的 HTML 结构准备好(如操作 DOM 元素)。
- 脚本之间有依赖关系,必须按顺序执行
<script src="main.js" defer></script>
<script src="utils.js" defer></script>
async
的使用场景:
- 独立脚本,比如广告、统计分析工具。
- 脚本不依赖 HTML 结构或其他脚本。
- 对执行顺序没有要求。
<script src="main.js" defer></script>
<script src="utils.js" defer></script>
为什么异步加载可以与 HTML 解析同时进行?
要结合浏览器架构来理解。
网络线程的作用
- 浏览器使用独立的网络线程来加载脚本文件。
- 当遇到
<script>
带有defer
或async
时:
- 主线程继续解析 HTML。
- 网络线程负责异步加载脚本。
任务队列机制
- 加载完成的脚本会被放入浏览器的任务队列中。对于
async
:加载完成的脚本会尽快被主线程执行,无需等待 HTML 解析完成。 - 对于
defer
:脚本会在 HTML 解析完成后按照顺序依次执行。
性能优化策略
- JS优化:
<script>
标签加上 defer属性 和 async属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。 defer属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。 async属性: HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。 - CSS优化:
<link>
标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能