一、浏览器组成
1. 对浏览器的理解
浏览器的主要功能是将用户选择的 web 资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是 HTML,也包括 PDF、image 及其他格式。用户用 URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。
HTML 和 CSS 规范中规定了浏览器解释 html 文档的方式,由 W3C 组织对这些规范进行维护,W3C 是负责制定 web 标准的组织。但是浏览器厂商纷纷开发自己的扩展,对规范的遵循并不完善,这为 web 开发者带来了严重的兼容性问题。
浏览器整体可以分为 两大核心部分:
shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。也有一些浏览器并不区分外壳和内核。从 Mozilla 将 Gecko 独立出来后,才有了外壳和内核的明确划分。
-
前端界面(Shell)
- 地址栏
- 菜单、按钮、前进后退
- 书签、下载、设置
- 窗口管理、标签页
作用:给用户操作,不负责页面渲染,调用内核来实现各种功能的。
-
浏览器内核(Core)—— 最重要
内核主要包含两大引擎
-
渲染引擎(Rendering Engine)
- 解析 HTML → 构建 DOM 树
- 解析 CSS → 构建 CSSOM 树
- 生成渲染树 → 布局(Layout)→ 绘制(Paint)→ 合成(Composite)
-
JS 引擎( JavaScript Engine)
- 解析、编译、执行 JavaScript
- 处理 DOM 操作、逻辑计算、事件循环
- 常见的JavaScript引擎有V8(Chrome)、SpiderMonkey(Firefox)和JavaScriptCore(Safari),它们都经过优化和升级,可以提供高性能的脚本执行能力。
1.3 V8引擎
3. 常见的浏览器内核比较
- Trident: 这种浏览器内核是 IE 浏览器用的内核,因为在早期 IE 占有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准支持不是很好。但是由于 IE 的高市场占有率,微软也很长时间没有更新 Trident 内核,就导致了 Trident 内核和 W3C 标准脱节。还有就是 Trident 内核的大量 Bug 等安全问题没有得到解决,加上一些专家学者公开自己认为 IE 浏览器不安全的观点,使很多用户开始转向其他浏览器。
- Gecko: 这是 Firefox 和 Flock 所采用的内核,这个内核的优点就是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,但是代价是也显而易见就是要消耗很多的资源,比如内存。
- Presto: Opera 曾经采用的就是 Presto 内核,Presto 内核被称为公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,在处理 JS 脚本等脚本语言时,会比其他的内核快3倍左右,缺点就是为了达到很快的速度而丢掉了一部分网页兼容性。
- Webkit: Webkit 是 Safari 采用的内核,它的优点就是网页浏览速度较快,虽然不及 Presto 但是也胜于 Gecko 和 Trident,缺点是对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会使一些编写不标准的网页无法正确显示。WebKit 前身是 KDE 小组的 KHTML 引擎,可以说 WebKit 是 KHTML 的一个开源的分支。
- Blink: 谷歌在 Chromium Blog 上发表博客,称将与苹果的开源浏览器核心 Webkit 分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎(即浏览器核心),内置于 Chrome 浏览器之中。其实 Blink 引擎就是 Webkit 的一个分支,就像 webkit 是KHTML 的分支一样。Blink 引擎现在是谷歌公司与 Opera Software 共同研发,上面提到过的,Opera 弃用了自己的 Presto 内核,加入 Google 阵营,跟随谷歌一起研发 Blink。
4. 常见浏览器所用内核
(1) Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink内核;
(2)IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;
(3) Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;
(4) Safari 浏览器内核:Webkit 内核;
(5) Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核;
(6) 360浏览器、猎豹浏览器内核:IE + Chrome 双内核;
(7) 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);
(8) 百度浏览器、世界之窗内核:IE 内核;
(9) 2345浏览器内核:好像以前是 IE 内核,现在也是 IE + Chrome 双内核了;
(10)UC 浏览器内核:这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核。
5. 浏览器的主要组成部分
-
用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗⼝显示的您请求的⻚⾯外,其他显示的各个部分都属于⽤户界⾯。
-
浏览器引擎 - 在⽤户界⾯和呈现引擎之间传送指令。
-
呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
-
网络 - ⽤于⽹络调⽤,⽐如 HTTP 请求。其接⼝与平台⽆关,并为所有平台提供底层实现。
-
用户界⾯后端 - ⽤于绘制基本的窗⼝⼩部件,⽐如组合框和窗⼝。其公开了与平台⽆关的通⽤接⼝,⽽在底层使⽤操作系统的⽤户界⾯⽅法。
-
JavaScript 解释器。⽤于解析和执⾏ JavaScript 代码。
-
数据存储 - 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“⽹络数据库”,这是⼀个完整(但是轻便)的浏览器内数据库。
值得注意的是,和⼤多数浏览器不同,Chrome 浏览器的每个标签⻚都分别对应⼀个呈现引擎实例。每个标签⻚都是⼀个独⽴的进程。
二、进程与线程
1. 进程与线程的概念
进程 : 进程是操作系统资源分配的基本单位,进程中包含线程。简而言之,就是正在进行中的应用程序。
线程:线程是由进程所管理的。是进程内的一个独立执行的单位,是CPU调度的最小单位。
进程是资源分配的最小单位,线程是CPU调度的最小单位。
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。进程是运行在虚拟内存上的 ,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间。
多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的。那什么又是进程呢?
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
-
线程是进程的基本单位,一个进程由一个或者多个线程组成,搞清楚这个关系之后,我们可以明确线程就是程序执行的最小单元。
-
线程和进程一样,也是动态概念,有创建有销毁,存在只是暂时的,不是永久性的。
-
进程与线程的区别在于进程在运行时拥有独立的内存空间,也就是说每个进程所占用的内存都是独立的。
- 例如:微信运行时,系统会给它一个运行内存。
-
多个线程是共享内存空间的,但是每个线程的执行是相互独立的,线程必须依赖于进程才能执行,单独的线程是无法执行的,由进程来控制多个线程的执行,没有进程就不存在线程。
- 例如:我先开启一个发送消息的线程,那么同时还能由一个接收消息的线程。两个线程之间完全独立。
如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存。
进程和线程之间的关系有以下四个特点:
-
进程中的任意一线程执行出错,都会导致整个进程的崩溃。
-
线程之间共享进程中的数据。
-
当一个进程关闭之后,操作系统会回收进程所占用的内存, 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
-
进程之间的内容相互隔离。 进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。
为了提升浏览器的稳定性和安全性,浏览器采取了多进程模型。
-
浏览器四大进程
Chrome浏览器进程架构图:
从图中可以看出,最新的 Chrome 浏览器包括:
-
1 个浏览器主进程
-
1 个 GPU 进程
-
1 个网络进程
-
多个渲染进程
-
多个插件进程
这些进程的功能:
-
浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
-
渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。不能读写硬盘上的数据,不能获取操作系统权限。(用多进程架构的额外好处是可以使用安全沙箱,你可以把沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据,例如你的文档和桌面。Chrome 把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。)
-
GPU 进程:其实, GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。这个进程可以调用硬件进行渲染,从而实现渲染加速。比如
translate3d等css3属性会骗取调用GPU进程从而开启硬件加速。 -
网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
-
插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
浏览器是多进程的,有一个主进程,每打开一个tab页面都会新开一个进程(某些情况下多个tab会合并进程)。
日常中我们使用浏览器是基于一个一个tab页进行访问网站,如果说某一个tab页面挂掉了其实对于其他tab页是没有任何影响的,其实每一个tab页就是一个单独的进程。
虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:
- 更高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
- 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
什么 “一个 Tab 一个进程”?
Chrome 等浏览器采用 “站点隔离(Site Isolation)” 策略,核心好处是:
- 稳定性:单个 Tab 崩溃不会影响其他页面或整个浏览器;
- 安全性:渲染进程在沙箱中运行,恶意代码难以逃逸;
- 性能:多核 CPU 并行处理,提升响应速度。
这就是你在任务管理器中看到大量 Chrome 进程条目的原因。
哪些情况下多个 Tab 会合并进程?
浏览器会根据策略合并进程,以平衡性能和资源占用:
-
同一站点(Same Site)
- 多个 Tab 打开的是同一站点(如
a.example.com和b.example.com),Chrome 可能会将它们合并到同一个渲染进程中。 - 这是因为同一站点的页面共享相同的安全上下文,合并后不会影响安全性。
- 多个 Tab 打开的是同一站点(如
-
后台标签页优化
- 当打开大量后台标签页时,Chrome 会将部分不活跃的后台标签页合并到一个 “共享渲染进程” 中,以减少内存占用。
- 这就是在任务管理器中看到部分 Chrome 进程处于 “效能模式” 的原因。
-
iframe 与子页面
- 页面内的 iframe 如果来自同一站点,通常会与父页面共享同一个渲染进程;
- 若 iframe 来自不同站点,则会创建独立进程(站点隔离)。
-
扩展程序与后台服务
- 扩展程序通常运行在独立进程中,但部分轻量扩展可能会合并到共享进程。
-
进程和线程的区别
-
进程可以看做独立应用,线程不能
-
资源:进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位);线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。
-
通信方面:线程间可以通过直接共享同一进程中的资源,而进程通信需要借助 进程间通信。
-
调度:进程切换比线程切换的开销要大。线程是CPU调度的基本单位,线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
-
系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、I/O 等,其开销远大于创建或撤销线程时的开销。同理,在进行进程切换时,涉及当前执行进程 CPU 环境还有各种各样状态的保存及新调度进程状态的设置,而线程切换时只需保存和设置少量寄存器内容,开销较小。
-
-
浏览器多进程的优势
-
-
相比于单进程浏览器,多进程有如下优点:
-
避免单个page crash影响整个浏览器
-
避免第三方插件crash影响整个浏览器
-
多进程充分利用多核优势
-
方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
-
简单点理解:如果浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差;同理如果插件崩溃了也会影响整个浏览器;而且多进程还有其它的诸多优势。当然,多进程,内存等资源消耗也会更大,有点空间换时间的意思。
-
-
浏览器渲染进程的线程
-
GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,JS 执行时,渲染暂停;渲染时,JS 不执行。
-
JS引擎线程(单线程)
- 也称为JS内核,负责处理Javascript脚本程序。(例如常常听到的谷歌浏览器的V8引擎,新版火狐的JaegerMonkey引擎等)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
-
事件触发线程
- 归属于渲染进程而不是JS引擎,用来控制事件轮询(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如鼠标点击、AJAX异步请求等,会将对应任务添加到事件触发线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理任务队列的队尾,等待JS引擎的处理
- 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
-
定时触发器线程
- 定时器setInterval与setTimeout所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的(因为JavaScript引擎是单线程的, 如果任务队列处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
-
异步http请求线程
- 用于处理请求XMLHttpRequest,在连接后是通过浏览器新开一个线程请求。如ajax,是浏览器新开一个http线程
- 将检测到状态变更(如ajax返回结果)时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入js引擎线程的事件队列中。再由JavaScript引擎执行。
5.6 合成线程(Compositor Thread)
- 图层划分、图块切分、栅格化
- transform /opacity 动画在这里执行,不卡主线程
5.7 栅格化线程(Raster Threads)
-
把图层变成位图
-
多线程并行,交给 GPU 加速
三、输入url地址到浏览器显示页面发生了什么
看一张HTTP 请求示意图,用来展现浏览器中的 HTTP 请求所经历的各个阶段。
接下来我们从进程角度讨论一下:从浏览器里,输入URL地址,到页面显示,这中间发生了什么?
从上图可以看到,整个过程需要各个进程之间的配合,我们结合上图我们从进程的角度,描述一下
- 浏览器进程接收到用户输入的URL请求,浏览器进程便将URL转发给网络进程。
- 网络进程中发起真正的URL请求。
- 网络进程接收到响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
- 浏览器进程接收到网络进程的响应头数据之后,发送 "提交文档" 消息到渲染进程。
- 渲染进程接收到"提交文档"的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道。
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
- 浏览器进程接收到渲染进程"确认提交"的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。
所谓提交文档,就是浏览器主进程,将网络进程接收到的HTML数据提交给渲染进程。
四、渲染主线程是如何工作的?
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画 60 次
- 执行全局 JS 代码
- 执行事件处理函数
- 执行计时器的回调函数
- ......
思考题:为什么渲染进程不适用多个线程来处理这些事情?
要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务?
比如:
- 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
- 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
- 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
- ......
渲染主线程想出了一个绝妙的主意来处理这个问题:队列
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
这样一来,就可以让每个任务有条不紊的、持续的进行下去了。
整个过程,被称之为事件循环(消息循环)
五、浏览器渲染流程
浏览器中的“渲染”指的是将HTML字符串转化为屏幕上的像素信息的过程。 我们可以将渲染想象成一个render函数,函数接收一个html字符串,将其经过一系列处理得出若干像素点的颜色,将这些像素信息存在pixels变量中返回。
渲染时间点
了解什么是渲染之后,我们不由得好奇发问:渲染是在什么时候发生的呢?
当我们在浏览器键入一个URL时,网络线程会通过网络通信拿到HTML,但网络线程自身并不会处理HTML(人家是专注于搞网络的),它会将其生成一个渲染任务交给消息队列,在合适的时机渲染主线程会从消息队列中取出渲染任务执行,启动渲染的流程。
-
HTML文档解析
渲染的第一步是解析 HTML文档。
解析过程中中遇到HTML元素会解析HTML元素最终生成DOM树,遇到 CSS 会下载并解析 CSS,遇到 JS会暂停解析HTML,而是去下载并执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。
因为浏览器无法直接理解和使用html,所以需要将html转换为浏览器能够理解的结构——DOM树。 在渲染引擎内部,有一个叫 HTML 解析器(HTMLParser)的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。
生成DOM树
解析的过程中遇到HTML元素会解析HTML元素最终生成DOM树;
生成CSSOM树
解析的过程中遇到style标签、link元素、行内样式等CSS样式,会解析CSS生成CSSOM树。
CSS不会阻塞HTML解析
如果主线程解析到link位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。
CSSOM构建:
- 不会阻塞DOM树的构建(现代浏览器)
- 但会阻塞渲染树构建(必须等待CSSOM完成)
- 会阻塞后续JavaScript执行(JS可能依赖样式)
JS会阻塞HTML解析
如果主线程解析到script位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。
-
JavaScript:
- 同步脚本(
<script>)会立即阻塞DOM构建 - 遇到脚本时,必须等待当前所有CSS下载完成才执行(避免JS操作未解析的样式)
- 同步脚本(
script标签中defer和async的区别
如果没有defer或async属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。
下图可以直观的看出三者之间的区别:
其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。
defer 和 async属性都是去异步加载外部的JS脚本文件,其区别如下:
-
执行顺序: 多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行;
-
async:遇到scirpt标签时,浏览器开始异步下载,下载完成后如果此时 HTML 还没有解析完,浏览器会暂停解析,先让 JS 引擎执行代码,执行完毕后再进行解析(可能会阻塞) -
defer:遇到scirpt标签时,浏览器开始异步下载,html页面解析完才执行js文件。(立即下载,但延迟执行(整个页面都解析完毕之后再执行,不阻塞)
第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
-
样式计算
渲染的下一步是样式计算。
经过HTML解析过后,我们拿到了DOM树和CSSOM树,但是光得到这两颗树还不够,还需要知道每个DOM对应哪些样式。
主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。
在这一过程中,很多预设值会变成绝对值,比如**red会变成rgb(255,0,0)** ;相对单位会变成绝对单位,比如**em会变成px**。
这一步完成后,会得到一棵带有样式的 DOM 树。
Tip:不了解CSS计算过程的同学,可以看一看这篇文章 # 你是否了解 CSS 的属性计算过程呢?
-
布局
接下来是布局,布局完成后会得到布局树。
布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、具体位置的x,y坐标,
有了样式信息,但是还不能绘制出来,因为还不知道具体的位置信息和尺寸信息
当修改了节点的几何属性,如大小、位置,就需要重新计算布局,这个过程也叫做回流或者重排(reflow)
获取节点的几何属性时,如 offsetWidth / getBoundingClientRect/clientWidth 会强制重排
大部分时候,DOM 树和布局树并非一一对应。
比如display:none的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
-
分层
下一步是分层。
- 主线程会使用一套复杂的策略对整个布局树中进行分层。
- 将页面进行分层,之后某个层变化时,就可以单独更新这一个图层,从而避免了全页面的更新,提高效率。
- 滚动条一般会单独分层
- 滚动条、堆叠上下文、
transform、opacity等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。
-
绘制
- 为每一个分层单独绘制对应的指令集,用来描述当前图层该如何绘制
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
-
分块
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
合成线程首先对每个图层进行分块,将其划分为更多的小区域。
它会从线程池中拿取多个线程来完成分块工作。
- 大尺寸图层(如全屏滚动区域)会占用大量的GPU内存,消耗资源效率低
- 合成线程会调用多个线程把每个分层都分成更小的块(Tile),通常为256x246或者512x512像素
-
光栅化
分块完成后,进入光栅化阶段。
页面可能很大,但用户只能看到一部分,在这种情况下如果全部绘制,就会产生很大的性能开销,因此需要优先绘制视口(即用户看到的区域)区域内的元素。
基于此原因,绘制前,合成线程会对页面进行分块,然后将每个图块发送给栅格线程,栅格线程将图块转换为位图。合成器线程可以优先处理不同的栅格线程,这样就可以首先对视口(或附近)中的事物进行栅格化。
光栅化是将每个块变成位图,位图可以理解成内存里的一个二维数组,这个二维数组记录了每个像素点信息。
合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。
GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
光栅化的结果,就是一块一块的位图。
-
画
最后一个阶段就是画了
合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。
指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。
变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。
合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。
总结
再让我们来回顾一遍完整过程:
- 解析HTML:将字符串解析成
DOM树和CSSOM树 - 样式计算:得到
Computed Style - 布局:产生布局树
- 分层:划分图层
- 绘制:产生绘制指令集
- 分块:划分区域
- 光栅化:生成位图
- 画:生成
quad,提交硬件,完成成像
解析HTML ————> 计算样式 ————> 布局 ————> 分层 ————> 绘制 ————> 分块 ————> 光栅化 ————> 画。
六、重排(Reflow)与重绘(Repaint)的优化:
-
避免触发重排:
- 避免频繁操作 DOM(使用
DocumentFragment或离线 DOM 进行批量修改)。 - 避免逐项修改样式,使用
class或cssText一次性修改。 - 避免在循环中读取会触发重排的布局属性(
offsetTop,offsetLeft,offsetWidth,offsetHeight,scrollTop,scrollLeft,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight,getComputedStyle())。如果必须读取,先将它们缓存起来。 - 对复杂动画元素使用绝对定位 (
position: absolute或fixed),使其脱离文档流,影响范围缩小。
- 避免频繁操作 DOM(使用
-
利用合成(Composition)优化动画: 优先使用
transform(位移、缩放、旋转) 和opacity属性来制作动画。这些属性可以由合成器线程直接在 GPU 上处理,跳过主线程的布局和绘制阶段,性能极高。 -
will-change属性: 提示浏览器哪些元素可能会发生变化(如transform,opacity),让浏览器提前为其创建独立的合成层,优化后续变化的性能(但要谨慎使用,滥用会增加内存消耗)。 -
content-visibility: auto: 现代 CSS 属性,可以跳过屏幕外内容的渲染(布局和绘制),大幅提升长页面加载和滚动性能。