# 🌏浏览器的工作原理及渲染机制
# 前言
做为一个前端工程师,每天都和浏览器打交道,所以了解浏览器内部的机制及其运作原理是非常有必要的。这篇文章主要阐述的浏览器的渲染机制,帮助我们更好的理解浏览器的运作原理。
# 🤸♂️ 知识准备
在展开渲染机制相关的内容之前,先来简单了解一下常见的浏览器内核以及线程与进程的概念
# (一)浏览器内核
浏览器内核是浏览器的核心,也称“渲染引擎”
,用来解释网页语法并渲染到网页上。浏览器内核决定了浏览器该如何显示网页内容以及页面的格式信息。不同的浏览器内核对网页的语法解释也不同,因此网页开发者需要在不同内核的浏览器中测试网页的渲染效果。
浏览器内核又可以分成两部分:渲染引擎(layout engineer
或者 Rendering Engine)和 JS 引擎
。它负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。JS 引擎则是解析 Javascript 语言,执行 javascript 语言来实现网页的动态效果。
目前市面上常见的浏览器内核主要有以下这些:
浏览器/RunTime | 内核(渲染引擎) | Javascript引擎 |
---|---|---|
Chrome | 统称为Chromium内核或Chrome内核,以前是Webkit内核,现在是Blink内核 | V8 |
FireFox | Gecko内核,俗称Firefox内核 | SpiderMonkey |
Safari | Webkit内核 | JavaScriptCore |
Edge | EdgeHTML | Chakra(for JavaScript) |
IE | Trident | Chakra(for JScript) |
Node.js | - | V8 |
这里面大家最耳熟能详的可能就是 Webkit
内核了,其中的 Blink
其实就是 Webkit
的一个分支。该篇文章们也会主要学习关于 WebKit 的这部分渲染引擎机制的相关内容。
# (二)线程和进程
这个章节不会展开详细说明线程和进程,因为在后续文章中会出现相关线程和进程的字眼。这里只是简单的阐述一下线程和进程。为了更好的理解,我在网上找到一篇用“非专业”术语来解释线程与进程的文章

【专业术语解释】
进程
是cpu资源分配的最小单位(是拥有独立资源和独立运行的最小单位)线程
是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
当我们启动一个应用的时候,计算机会创建一个进程,操作系统会为该应用分配一片独立的内存空间,应用内的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作。这些线程共享该应用内存中资源。如果应用关闭,进程会被终结。操作系统会释放相关内存。
【非专业术语解释】
- 工厂的资源 -> 系统分配的内存(独立的一块内存)
- 工厂之间的相互独立 -> 进程之间相互独立
- 多个工人协作完成任务 -> 多个线程在进程中协作完成任务
- 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
- 工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
# 一、 浏览器的组成结构
浏览器的组成结果主要分为以下几个部分:
- **用户界面:**包括地址栏、前进/后退 按钮、书签菜单等,除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面
- **浏览器引擎:**在用户界面和呈现引擎之间传送指令
- 渲染引擎:负责显示请求的内容,如果请求的内容是
HTML
,它就负责解析HTML
和CSS
内容,并将解析后的内容显示在屏幕上 - **网络:**用于网络调用,比如
HTTP
请求,其接口与平台无关,并为所有平台提供底层实现 - **用户界面后端:**用于绘制基本的窗口小部件,比如组合框和窗口,其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法
- **
JavaScript
解释器:**用于解析和执行JavaScript
代码 - **数据存储:**这是持久层,浏览器需要在硬盘上保存各种数据,例如
Cookie
,新的HTML
规范(HTML5
)定义了『网络数据库』,这是一个完整(但是轻便)的浏览器内数据库
# 二、浏览器的进程和线程
有了上面知识的铺垫,下面来了解一下浏览器里的进程和线程。不同的浏览器采用不同的架构模式,这里并不存在标准,本文以Chrome为例进行说明
# 1. 多进程的浏览器
浏览器是属于多进程架构,其中包括了最重要的浏览器渲染进程(浏览器内核),比如留在浏览器中新开了一个TAB页面,就相当于新开了一个渲染进程。下面这两张可爱的图片说明的了览器进程的主要组成部分以及不同进程负责的浏览器区域


- 主进程(Browser Process)
- 浏览器界面的显示、用户的交互(前进、后退、收藏)
- 协调控制其他子进程的创建和销毁
- 处理一些不可见的操作,如网络请求,文件访问等
- **渲染进程(Renderer Process)**✨✨✨
- 是浏览器内最核心的部分也成为浏览器内核
- 负责一个tab页面内关于网页呈现的所有事情(页面渲染、脚本执行、事件处理)
- GPU进程(GPU Process)
- 用于进行3D绘制
- 第三方插件进程(Plugin Process)
- 负责伊特网页用到的所有插件,如flash
- 每个类型的插件对应一个进程,仅当使用插件时才创建
# 2. 渲染进程内的线程
上面阐述一些关于浏览器进程的内容,在其中最重要的是渲染进程(浏览器内核),了解其中的构成对后期学习事件轮询以及异步方面的知识有很大的帮助。
**进程和线程是一对多的关系,**也就是说一个进程包含了多条线程,而对于渲染进程来说,它当然也是多线程的了,接下来我们来看一下渲染进程包含哪些线程
🚑【注意】GUI渲染线程和JS引擎线程互斥
这里详细解析一下为什么GUI渲染线程和JS引擎线程是互斥的。由于Javascript是可以操作DOM元素的。如果在修改这些元素的属性的同时渲染界面(即JS线程和GUI线程同时运行),可能会导致渲染线程前后获得的元素数据不一致。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程和JS引擎线程之间为互斥的关系。其中一者运行另一者就会被挂起。
因此如果 JavaScript
执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉
# 三、页面加载流程
在了解浏览器渲染过程之前,先简单了解一下页面的加载流程。有助于更好的理解后续的渲染过程。从浏览器地址中输入url地址到渲染出一个页面,会经过以下过程:
【推荐文章】在浏览器输入 URL 回车之后发生了什么(超详细版)
- 浏览器输入的url地址经过DNS解析获得对应的IP
- 向服务器发起TCP的三次握手
- 建立链接后,浏览器向该IP地址发送http请求
- 服务器接收到请求后,返回一推HTML格式的字符串代码
- 浏览器获取到htm代码,解析成DOM树
- 获取CSS并构建CSSOM
- 将DOM与CSSOM结合,创建渲染树
- 找到所有的内容都处于网页的哪个位置,布局渲染树
- 最终绘制出页面
# 四、 页面渲染过程
上面简答了解了用户从地址栏输入url回车后到最终渲染出整个页面的过程。浏览器的渲染过程主要是上述步骤的5-9。这里展开详细说一下在浏览器的渲染页面过程中都发生了什么。

接下来阐述的内容就可以抽象成上面这张图
# 1. 浏览器获取到htm代码,解析成DOM树
- 当我们打开一个网页时,浏览器都会去请求对应的 HTML 文件。虽然平时我们写代码时都会分为 JS、CSS、HTML 文件,也就是字符串,但是计算机硬件是不理解这些字符串的,所以在网络中传输的内容其实都是
0
和1
这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。
- 当数据转换为字符串以后,浏览器会先将这些字符串通过词法分析转换为标记(token),这一过程在词法分析中叫做标记化(tokenization)
- 当结束标记化后,这些标记会紧接着转换为 Node,最后这些 Node 会根据不同 Node 之前的联系构建为一颗 DOM 树
# 2. CSS转换成CSSOM树
上面的整个过程是在对HTML进行解析,有了HTML的解析那么CSS的解析也是必不可少的。解析CSS构建CSSOM和构建DOM的过程十分相似
在构建的过程中浏览器需要递归CSSOM树,然后确定元素长什么样子
CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,不要过渡层叠下去 所以,CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度,因此在默认情况下CSS被视为阻塞渲染的资源
# 3. 生成渲染树(Render Tree)
当生成DOM树和CSSOM树后,需要将这两颗树进行合并生成渲染树具体的合并过程如下:
- 从DOM树的根节点开始遍历每个可见的节点
- 有些节点不可见(例如脚本、Token等),因为它们不会出现在渲染输出中,所以会被忽略
- 某系节点被CSS隐藏,因此在渲染树中也会被忽略。例如某些节点设置了display: none
- 对于每个可见的节点,为其找到适配的CSSOM规则并应用它们
# 4. 在渲染树的基础上进行布局
当生成渲染树之后,浏览器就会知道网页中有哪些节点、各个节点的CSS。就会从渲染树的根节点开始进行遍历,入后确定节点对象在页面上确切的大小和位置。
# 5. 把每个节点绘制到屏幕上
布局完成后,浏览器已经知道了哪些节点要显示、每个节点的 CSS
属性是什么、每个节点在屏幕中的位置是哪里,就进入了最后一步绘制阶段
,按照算出来的规则,通过显卡,把内容画到屏幕上
# 五、 渲染阻塞
渲染的前提是生成渲染树,所以HTML和CSS的解析必定会造成渲染的阻塞。如果想要渲染阻塞的时间小,就应该降低一开始需要进行渲染的文件大小。
首先先说结论,在解析HTML文件时<script>
标签会阻塞HTML的解析,而<link>
标签不会。但是<link>
标签会阻塞页面的渲染,而且会阻塞JS的执行。那么为什么是这样的呢?
在解析HTML时,当遇到
<script>
标签时,就会去停止页面的解析,去请求脚本文件并执行。因此会阻塞HTML的解析。DOM解析和CSS解析是两个并行的进程,所以这也解释了为什么CSS加载不会阻塞DOM的解析。
然而,由于Render Tree是依赖于DOM Tree和CSSOM Tree的,所以他必须等待到CSSOM Tree构建完成,也就是CSS资源加载完成(或者CSS资源加载失败)后,才能开始渲染。因此,CSS加载是会阻塞Dom的渲染的。
由于JS可能会操作之前的DOM节点和CSS样式,因此浏览器会维持HTML中CSS和JS的顺序。因此,样式表会在后面的JS执行前先加载执行完毕。所以CSS会阻塞后面JS的执行。
所以script标签的位置十分重要,在实际的使用过程中一般遵循以下两个原则:
- CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
- JS置后:我们通常把JS代码放到页面底部,且JavaScript 应尽量少影响 DOM 的构建
# 六、 性能优化策略
在了解了浏览器的渲染机制后,可以针对DOM和CSSOM的构建顺序给出一些性能优化的策略。
# 1. async 和 defer
上面内容说到了遇到scipr标签时,会阻塞HTML的解析。所以尽量把script标签放在body的底部。但是script有两个属性defer和async。加上这两个属性后,JS文件可以和HTML并行解析,这样可以把script标签放在任意的位置。下面说一下这两个属性之间的区别。
其中蓝色线代表 JavaScript
加载,红色线代表 JavaScript
执行,绿色线代表 HTML
解析,所以我们也对应的分别来看看三种情况
- 没有加
async
和defer
属性,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script src="request.js"></script>
async
属性表示异步执行引入的JavaScript
,与defer
的区别在于,如果已经加载好,就会开始执行,即无论此刻是HTML
解析阶段还是DOMContentLoaded
触发之后,不过需要注意的是,这种方式加载的JavaScript
依然会阻塞load
事件,换句话说,async-script
可能在DOMContentLoaded
触发之前或之后执行,但一定在load
触发之前执行。
<script src="request.js" async></script>
defer
属性表示延迟执行引入的JavaScript
,即这段JavaScript
加载时HTML
并未停止解析,这两个过程是并行的,整个document
解析完毕且defer-script
也加载完成之后(这两件事情的顺序无关),会执行所有由defer-script
加载的JavaScript
代码,然后触发DOMContentLoaded
事件
<script src="request.js" defer></script>
# 2. 重绘(Repaint)和回流(Reflow)
当元素的样式发生变化的时候,浏览器需要触发更新,重新绘制元素。这个过程中有两种类型的操作,分别时重绘(Repanit)和回流(Repaint)

# (1)什么是重绘和回流
【重绘Repaint】:当元素的样式改变不影响布局时(比如修改color),浏览器将使用重绘对元素进行更新,此时由于只需要UI层面重新绘制,因此对性能的损耗较小。
【回流Reflow】: 当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,会触发回流。该操作对性能的损耗较大。
✨✨ 重绘不一定触发回流,但是回流一定会触发重绘。回流比重绘的成本高的多
# (2)什么操作会触发重绘和回流
✅ 触发回流的属性和方法
任何可以改变元素几何信息(元素的位置和尺寸)的操作,都会触发回流
具体表现为:
- 添加或者删除可见的
DOM
元素 - 元素尺寸改变(边距、填充、边框、宽度和高度)
- 内容变化,比如用户在
input
框中输入文字 - 浏览器窗口尺寸改变(
resize
事件发生时) - 计算
offsetWidth
和offsetHeight
属性 - 设置
style
属性的值
✅ 触发重绘的属性和方法
# (3)如何减少重绘和回流
- 使用
visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局) - 不要使用
table
布局,可能很小的一个小改动会造成整个table
的重新布局 - 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
- 将动画效果应用到position属性为absolute或fixed的元素上
- 集中改变样式,即通过改变
class
的方式来集中改变样式
// 判断是否是黑色系样式
const theme = isDark ? 'dark' : 'light'
// 根据判断来设置不同的class
ele.setAttribute('className', theme)
- 使用
DocumentFragment
,我们可以通过createDocumentFragment
创建一个游离于DOM
树之外的节点,然后在此节点上批量操作,最后插入DOM
树中,因此只触发一次回流
var fragment = document.createDocumentFragment()
for (let i = 0; i < 10; i++) {
let node = document.createElement('p')
node.innerHTML = i
fragment.appendChild(node)
}
document.body.appendChild(fragment)
- 使用
transform
替代top
<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>
- 不要把节点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
# 3. 首屏加载优化
- **减少首屏CGI的计算量:**比如在微信8.8无现金日H5开发中,前端希望拿到用户的个人信息、消费记录、排名三类数据,如果只通过一个CGI来处理,那么后台响应时间肯定会变长;由于在H5的首屏中,只包含了用户信息,消费记录、排名都在第2屏和第3屏,此时其实可以利用异步的方式来拿消费记录、排名的数据。
- **页面瘦身:**压缩HTML、CSS、JavaScript。
- **减少请求:**CSS、JavaScript文件数尽量少,甚至当CSS、JS的代码不多时,可以考虑直接将代码内嵌到页面中。
- **多用缓存:**缓存能大幅度降低页面非首次加载的时间。
- **少用table布局:**浏览器在渲染table时会消耗较多资源,而且只有table里有一点变化,整个table都会重新渲染。
- **做预加载:**部分H5页面首屏可能要下载较多的静态资源,比如图片,这时为了避免加载时出现“难看”的页面,用预加载(loading的方式)做一个过渡
# 总结
该篇文章从浏览器的组成结构展开,阐述了浏览器内部的进程和线程。并重点了解了浏览器的渲染进程(浏览器内核)部分,浏览器的渲染进程中有多个线程在工作,其中的JS引擎线程和GUI渲染线程引出后续的重点——浏览器渲染页面的过程。并讲述了在渲染过程中会阻塞页面渲染的情况,并给出了一定的优化策略。