写在前面
要研究浏览器渲染的原理,我们首先要知道浏览器的主要组成部分 和浏览器的工作过程。
- 浏览器的主要组成部分
用户界面
:包括标签页、地址栏、导航按钮、收藏夹、书签等,用于用户与浏览器进行交互。JavaScript引擎
:JavaScript引擎负责解释和执行网页的 JavaScript 代码。它将 JavaScript 代码转换为计算机能理解的指令,从而实现用户的指定操作以及网页的动态交互。常见的 JavaScript 引擎包括:- V8(Google Chrome、Microsoft Edge)
- SpiderMonkey(Mozilla Firefox)
- Nitro,也叫做 JavaScriptCore(Apple Safari)
- Trident(Internet Explorer(但IE浏览器已经无了))
渲染引擎
:渲染引擎负责解析和呈现网页内容。它将HTML、CSS和JavaScript等网页元素转换为用户可见的界面。常见的渲染引擎包括:- Blink(Google Chrome、Microsoft Edge、Chromium 项目(新版Opera就是基于Chromium 项目的))
- WebKit(Apple Safari、老版本的Chrome)
- Gecko(Mozilla Firefox)
- Trident(Internet Explorer(但IE浏览器已经无了))
布局引擎
:布局引擎(属于渲染引擎的一部分)负责处理网页中的排版和布局,将HTML和CSS转换为页面可见的结构,主要的功能包括计算元素的位置、大小、样式等。网络引擎
:网络引擎负责处理网络请求和响应,通过HTTP或HTTPS协议与服务器通信,将网页的内容下载到本地显示。数据存储
:浏览器可以存储一些用户数据,如Cookie、缓存文件、WebStorage、IndexedDB等。这些数据有助于提高网页加载速度和用户体验。插件和扩展
:浏览器允许用户安装插件和扩展,以增加额外的功能和特性。例如,广告拦截器、密码管理器和开发者工具等。安全组件
:浏览器具有安全功能,如阻止恶意网站、标识安全连接和隐私保护等,以确保用户在浏览网页时的安全性和隐私性。其他辅助功能组件
:例如下载管理、历史记录、打印预览等组件
- 浏览器的工作过程
- 用户输入网址或者搜索内容
- 向服务器发起请求
- 获取页面资源
- 渲染页面
- 执行JavaScript
- 呈现页面
浏览器渲染过程
- 获取HTML资源文件:浏览器通过网络请求获取HTML资源文件
- 构建DOM树:浏览器将 HTML 解析成 DOM(文档对象模型) 树,用于表示网页结构和内容。
- 构建CSSOM树:浏览器将 CSS 解析成 CSSOM(CSS对象模型)树,用于表示样式规则和层叠关系。
- 合并DOM树和CSSOM树,构建渲染树:浏览器将DOM树和CSSOM树合并,生成渲染树(Render Tree),渲染树只包含要显示的元素及其样式(display: none; 的元素不会出现在渲染树中)。
- 布局(Layout) :在布局阶段,浏览器会确定每个元素在屏幕上的精确位置和尺寸,考虑到元素的盒模型、浮动、定位等因素。这个阶段也被称为 回流(Reflow)。
- 绘制(Painting) :在绘制阶段,浏览器会遍历Render树并根据样式信息绘制每个元素的内容,将它们转化为屏幕上的像素。这个阶段也被称为 重绘(Repaint)。
- 合成与显示:绘制完成后,浏览器会将绘制好的像素传输到图形卡或者操作系统的图形库中,然后在屏幕上显示出来,呈现给用户。
构建DOM树
- 词法分析。解析器将HTML或者XML代码分解为一个又一个的标记(tokens),比如开始标签、结束标签、属性、文本节点、注释节点等。
- 语法分析。解析器将词法分析得到的标记组合成由一个节点构成的树状结构,整个过程,解析器会根据语言的规则,识别出文档树的结构。
构建CSSOM树
图片来源于blog.csdn.net/cune1359/ar...
浏览器在解析HTML过程中,如果遇到 link 或者 style 就会开始解析 CSS 并构建 CSSOM 树 构建 DOM 树和构建 CSSOM 树是可以并行进行的,两者都发生在浏览器的主渲染进程中。
需要注意的是构建 DOM 树和构建 CSSOM 树是一个比较消耗性能的过程,不同的 CSS 选择器在构建 CSSOM 树时的性能消耗也有不同,CSS 选择器尽量使用 id 选择器和 class 选择器,而且尽量不要过度嵌套。
构建渲染树
当 DOM 树和 CSSOM 树构建完后,就会将二者进行合并,但是合并并不是简单地组合,因为渲染树只包括要显示的元素,因此如果某个元素设置了 display:none
样式,该元素就不会出现在渲染树中。
对比一下 display:none
和 visibility: hidden
的区别:
- display:none
- 元素不可见,元素会被从文档流中移除(不会渲染)
- 鼠标悬停和点击无效,伪元素也会被隐藏
- 可以通过 js 获取该元素
- visibility: hidden
- 元素不可见,但是仍然占据着页面布局空间
- 鼠标悬停和点击无效,伪元素也会被隐藏
- 可以通过 js 获取该元素
在构建渲染树时,未匹配的 css 规则将会被忽略,优先级被覆盖的规则也会被忽略。
布局
布局阶段,浏览器根据渲染树的节点,计算出 DOM 节点的宽高和位置坐标等信息,生成布局树。布局树中包含伪元素、匿名行盒、匿名块盒等,其生成过程中涉及到 CSS 盒模型、浮动、定位等布局规则。布局过程不是一次性的,它会根据浏览器窗口变化、动态加载等原因重新执行,一般这个过程也称回流。
绘制
在布局完成后,渲染引擎根据渲染树的结构和布局信息,生成绘制指令集(Painting Command)。绘制指令集包含了绘制每个可见元素的具体指令,例如绘制矩形、文本、图像等。
合成并呈现
绘制完成后,浏览器会将绘制好的像素传输到图形卡或者操作系统的图形库中,然后在屏幕上显示出来,呈现给用户。
疑惑解答
浏览器在渲染时遇到JS文件应该怎么处理?
由于 JavaSCript 脚本的执行可能会影响 DOM 结构和内容,为了防止出现意外情况,通常 JavaSCript的加载、解析和执行都会阻塞文档的解析 ,也就是说,当浏览器在构建 DOM 树时,遇到 <script>
标签就会暂停 DOM 树构建(也有特殊情况: async-script
和 defer-script
,下面会说明)。
JavaScript 的加载和执行不仅会阻塞 DOM 的构建,也会间接导致 CSSOM 的构建阻塞 DOM 的构建。 因为 JavaScript 不仅可以操作 DOM,也可以修改样式,当 JavaScript 试图读取或者修改样式时,就需要等待 CSSOM 构建完成,这样才能保证 JavaScript 读取到的元素样式是最新。
一般建议将 <script>
标签放到 HTML 元素的后面 </body>
结束标签之前。这样可以确保 JavaScript 不会阻塞 DOM 树的构建并且可以保证 JavaScript 可以顺利操作 DOM,对于用户来说这可以减少白屏的时间,提升用户体验。
为了解决这种阻塞问题,HTML5 引入了 async 和 defer 两个属性来控制脚本的加载和执行。
对比 <script>
标签的 3 种情况的区别:
- 同步执行脚本
html
<script src="script.js"></script>
在默认情况下,当浏览器遇到这种同步加载的 <script>
标签时,它会阻塞页面的渲染和解析,直到脚本文件被下载、解析和执行完成。这会导致页面渲染的暂停,直到脚本加载和执行完毕。
- 异步执行脚本 async。async 属性只对外部脚本生效。
html
<script async src="script.js"></script>
使用 async
属性,浏览器会在解析到这个 <script>
标签时,继续解析和渲染页面,而不会等待脚本下载和执行完成。这意味着页面的渲染不会被阻塞,但脚本加载完成后会立即执行,即 异步下载,同步执行 。当 async-script
脚本加载完成,但 HTML 解析还没结束时,那么这时执行的 async-script
就会阻塞页面的渲染。async-script
保证会在页面的 load
事件前执行,但是可能会在 DOMContentLoaded
事件之前或者之后执行(这两个事件接下来会提到)。
需要注意的是,多个异步脚本之间的执行顺序是不确定的,取决于其加载完成的顺序。
- 延迟执行脚本 defer。defer 属性只对外部脚本生效。
html
<script defer src="script.js"></script>
和 async-script
脚本一样,defer-script
脚本的加载可以和 HTML 的解析并行,不同的是defer-script
脚本在加载完成后并不会立即执行,而是等待 HTML 解析完成后再执行(浏览器解析到 </html>
结束标签后才执行),即 DOM 树构建完成后就会立即执行。
defer-script
脚本会在DOMContentLoaded
事件之前执行。多个defer-script
脚本会按照在文档出现的顺序依次执行。
DOMContentLoaded 和 load
DOMContentLoaded
事件会在 DOM 树构建完成后立即执行。这时候所有的 HTML 元素都已经转化为 DOM 了,但是其他的外部资源(样式表、图片等)可能还没加载完成。这个事件标志着这时候可以操作 DOM 了。
js
document.addEventListener('DOMContentLoaded', () => {});
load
事件会在页面完全加载后触发,这代表了页面所有的外部资源(样式表、图片、脚本等)都已经加载完成了。在此事件中,可以确保页面的所有元素和资源都已准备就绪,可以执行与页面展示和交互有关的操作。
js
window.addEventListener('load', () => {});
构建DOM树和构建CSSOM可以并行执行吗?
是的,通常构建 DOM 树和构建 CSSOM 是可以并行执行的。因为 DOM 树和 CSSOM 是两个独立的数据结构,在解析 HTML 的时候,浏览器可以同时下载 CSS 资源并解析构建,前提是没有 JavaScript 阻塞。
为什么 JavaScript 操作 DOM 慢?
因为 JavaScript 在 JS 引擎进程,而 DOM 在渲染进程中,所以 JavaScript 操作 DOM 是一个跨进程的的任务,既然是跨进程通信肯定会存在一定的通信开销的,所以 JavaScript 操作 DOM 会慢。
如何减少首屏渲染的速度?
- 减少HTTP请求次数:合并或减少资源文件的数量,使用CSS雪碧图合并小图标。
- 合理使用浏览器缓存:使用浏览器缓存来存储资源,使得用户首次访问后,再次访问时可以直接从缓存加载资源,减少网络请求。
- 优化图片:合理使用图片格式或者通过压缩图片,减少图片的大小。
- 按需加载资源:对于首屏用不到的资源就尽量不加载,对于图片资源可以利用 data-src 代替 src 实现图片懒加载。
- 压缩资源文件:对CSS和JS等资源文件进行压缩,去除代码中的注释、多余的空格、不必要的换行等。
- 使用内容分发网络(CDN)技术: 使用内容分发网络(CDN)来分发网页资源,以确保资源能够从离用户更近的服务器加载,从而提高加载速度。
- 异步加载脚本: 将不影响页面初始化的 JavaScript 代码推迟到页面加载后再执行,以确保首屏渲染不受阻碍。
什么是回流(重排)?
回流(Reflow ),又称重排。当浏览器重新计算网页上某些元素的位置、宽高、布局时就会发生重排。
触发回流的情况
- 页面首次渲染。
- 浏览器窗口大小发生改变时(resize 事件发生时)。
- 增减、移除、显示、隐藏页面元素
- 页面元素的位置发生改变
- 页面元素的尺寸发生改变(边距、填充、边框、宽度和高度)
- 页面元素的内容发生改变(包括输入框内容发生改变)
- 读取元素的
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
、clientTop
、clientLeft
、clientWidth
、clientHeight
、scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
- 调用
window.getComputedStyle()
方法
怎么减少回流(重排)?
- 尽量避免多次单独修改DOM元素。可以通过创建文档碎片来代替多次增加DOM节点的操作;当一定要多次修改DOM时,可以先将DOM隐藏后再操作,操作完成后再恢复。
- 使用 CSS 动画。CSS 动画(例如 transform 和 opacity)不会触发回流,因为它们只涉及到图层的重绘,而不是布局的改变。transform 会调用GPU硬件加速。
- 避免使用table元素和table布局 。因为 table 中每个元素的大小以及内容的改动,都会导致整个 table 的重新计算。
- 使用文档碎片。在 DOM 中预先创建一个文档碎片,将多个操作都在文档碎片中完成,然后再一次性添加到文档中,减少实际的 DOM 修改次数。
- 批量修改样式。避免频繁单个修改 DOM 元素的样式,可以将多个样式修改合并成一个操作,减少回流次数。(例如:直接修改元素的 class 等)
- 使用绝对定位或固定定位。这可以将元素脱离文档流,减少对其他元素布局的影响。
- 避免频繁的布局查询。 尽量避免使用会触发布局查询的属性,比如 offsetTop、offsetLeft、clientWidth 等
什么是重绘?
重绘(Repaint)是指在页面上某些元素的可见部分发生变化时,浏览器需要重新绘制这些元素的过程。回流一定会发生重绘,发生重绘不一定会发生回流。
常见引起重绘属性和方法(但不引起重排的):
color | border-style | visibility | text-decoration |
background | background-image | background-position | background-repeat |
background-size | outline-color | outline | outline-style |
outline-radius | outline-width | box-shadow |