一文通透从输入URL到页面渲染的全过程----高频面试
喜欢大海
喜欢夕阳
写下便是永恒
文章目录
-
- 一文通透从输入URL到页面渲染的全过程----高频面试
- 重温
- 进程与线程
-
- 什么是进程
- 什么是线程
- 进程和线程的区别
- 多进程和多线程
- JS为什么是单线程
- 浏览器相关
-
- 浏览器是多进程的
- 浏览器包含哪些进程
-
- Browser进程
- 第三方插件进程
- GPU进程
- 渲染进程(重)
- 网络进程(Network process)
- 为什么浏览器要多进程
- 简述渲染进程Renderer(重)
-
- 渲染进程Renderer的主要线程
-
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 定时触发器线程
- 异步http请求线程
- 渲染过程
-
- Webkit渲染流程图
- 解析HTML构建DOM树
- 解析CSS构建CSSOM树
- 解析JavaScript脚本
- 构建渲染树/呈现树(Render Tree)
- 布局(Layout)
- 绘制(Painting)
-
- 重绘(Repaint)
- 回流 (Reflow)
- 什么会引起回流
- 如何减少和避免回流重绘
- 合成(Composite)
-
- 浏览器渲染方式
- 软件渲染技术
- 硬件加速技术
- 普通图层和复合图层
- 何为复合图层/硬件加速
- absolute?
- 页面渲染优化
- 最后
重温
从输入 URL 到页面渲染发生了什么?
比如在浏览器输入了 www.qq.com 后浏览器是怎么把最终的页面呈现。因为涉及到的知识点和可挖掘的地方比较多,而且这中间几乎每一步都是可以优化的。URL从输入到页面渲染这整个过程可以分为 网络通信 和 页面渲染 两个方面,一般后端程序猿回答这个问题侧重网络通信就行了,前端页面渲染也得了解。
总体六个流程
- 浏览器通过域名找出其IP地址(DNS解析)
- 浏览器和服务器建立连接(TCP/TP三次握手)
- 浏览器向服务器发送HTTP请求
- 服务器接受到请求并返回HTTP响应
- 浏览器解析渲染页面
- 断开连接(四次挥手)
说说页面渲染的流程,也就是当输入一个URL拿到了页面后,浏览器怎么解析,怎么呈现?
首先要了解这块内容,需要对下面这些知识点有一个简单认知
- 线程/进程概念及区别
- 多线程/多进程概念
- 浏览器的主要进程
- 浏览器为什么是多进程
- 渲染进程Renderer的主要线程
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 定时触发线程
- 异步http请求线程
- 渲染进程的各个线程之间关系及配合
进程与线程
什么是进程
我们都知道,CPU是计算机的核心,承担所有的计算任务
官方说法,进程是CPU资源分配的最小单位
字面意思就是进行中的程序,将它理解为一个可以独立运行且拥有自己的资源空间的任务程序
进程包括运行中的程序和程序所使用到的内存和系统资源
CPU 可以有很多进程,我们的电脑每打开一个软件就会产生一个或多个 进程 ,为什么电脑运行的软件多就会卡,是因为 CPU 给每个 进程 分配资源空间,但是一个 CPU 一共就那么多资源,分出去越多,越卡,每个进程之间是相互独立的, CPU 在运行一个 进程 时,其他的进程处于非运行状态,CPU 使用 时间片轮转调度算法 来实现同时运行多个进程
在分时系统中都采用时间片轮状算法进行进程调度。时间片是指一个较小的时间间隔,通常为 10 ms~100 ms 。在简单的轮转算法中,系统将所有的就绪进程按 FIFO 规则排成一个队列,将 CPU 分配给队首进程,且规定它最多只能连续执行一个时间片,若时间片用完时进程仍未完成,也必须将其插入就绪队列末尾,并把 CPU 交给下一个进程。时间片轮转法只用于进程调度,它属于抢占调度方式,其特点是简单易行、平均相应时间短,但它不利于处理紧急作业。
React Fiber的运行时实际上就是RequestIdelCallback(rIC)+ 优先级抢占(固然由于RequestIdelCallback取决于设备的Vsync信号发射频率,会形成不一样设备间的差别,所以优先使用polyfill,这个咱们暂时不展开细讲)。
类比系统的进程调度就是轮转(RR)+ 最短完成时间优先(STCF),只是将最短完成时间优先替换为业务须要的优先级,在单个时间片内,寻找最优先级的抢占式调度。
固然React Fiber自己还存在其余的优化策略,例如超时机制、任务的可中断,挂起,恢复、Concurrent模式等,咱们在次不一一展开讨论。
什么是线程
线程 是 CPU 调度的最小单位
线程 是建立在 进程 的基础上的一次程序运行单位,通俗点解释 线程 就是程序中的一个执行流(执行逻辑),一个 进程 可以有多个 线程
一个 进程 中只有一个执行流称作 单线程 ,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行
一个 进程 中有多个执行流称作 多线程,即在一个程序中可以同时运行多个不同的 线程 来执行不同的任务, 也就是说允许单个程序创建多个并行执行的 线程 来完成各自的任务
进程和线程的区别
进程是操作系统分配资源的最小单位,线程是程序执行的最小单位
一个 进程 由一个或多个 线程 组成,线程 可以理解为是一个进程中代码的不同执行路线
进程 之间相互独立,但同一进程下的各个 线程 间共享程序的内存空间 (包括代码段、数据集、堆等) 及一些进程级的资源 (如打开文件和信号)
调度和切换:线程上下文切换比进程上下文切换要快得多
多进程和多线程
多进程: 多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的。
比如大家可以在网易云听歌的同时打开编辑器敲代码,编辑器和网易云的进程之间不会相互干扰
多线程: 多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务
并行和并发https://www.elecfans.com/dianzichangshi/20171208597812.html
来个比喻:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。
JS为什么是单线程
JS的单线程,与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题
比如,假定 JavaScript 同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
还有人说 js 还有 Worker 线程,对的,为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许 JavaScript 脚本创建多个线程,但是子线程是完 全受主线程控制的,而且不得操作DOM
所以,这个标准并没有改变JavaScript是单线程的本质:JS是单线程的,即js的代码只能在一个线程上运行,也就说,js同时只能执行一个js任务
- 单线程的含义是js只能在一个线程上运行,也就说,js同时只能执行一个js任务,其它的任务则会排队等待执行。
- 多线程之间会共享运行资源,浏览器端的js会操作dom,多个线程必然会带来同步的问题,所以js核心选择了单线程来避免处理这个麻烦。
- js可以操作dom,影响渲染,所以js引擎线程和GUI线程是互斥的。这也就解释了js执行时会阻塞页面的渲染。
了解了进程和线程之后,接下来看看浏览器解析,浏览器之间也是有些许差距的,不过大致是差不多的,下文我们皆用市场占有比例最大的Chrome为例
浏览器相关
浏览器是多进程的
作为前端,免不了和浏览器打交道,浏览器是多进程的,拿 Chrome 来说,我们每打开一个Tab页就会产生一个进程,我们使用 Chrome 打开很多标签页不关,电脑会越来越卡,不说其他,首先就很耗CPU
浏览器包含哪些进程
最新的Chrome浏览器包括那些进程?
至少四个:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程和一个渲染进程
关于如何构建 web 浏览器并不存在标准规范,一个浏览器的构建方法可能与另一个迥然不同。不同浏览器的进程/线程架构一般由下图几部分:
Browser进程
- 浏览器的主进程(负责协调、主控),该进程只有一个
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将渲染(Renderer)进程得到的内存中的Bitmap(位图),绘制到用户界面上
- 网络资源的管理,下载等
第三方插件进程
- 每种类型的插件对应一个进程,当使用该插件时才创建
GPU进程
- 该进程也只有一个,用于3D/动画绘制等等
渲染进程(重)
- 即通常所说的浏览器内核(Renderer进程,内部是多线程)
- 每个Tab页面都有一个渲染进程,互不影响
- 主要作用为页面渲染,脚本执行,事件处理等
网络进程(Network process)
- 负责网络资源加载
utility进程(Utility Process)
有时候浏览器主进程需要做一些“危险”的事情,比如图片解码、文件解压缩。如果这些“危险”的操作发生了失败,会导致整个主进程发生异常崩溃,这是我们不愿意看到的。因此Chromium设计出了一个utility进程的机制。主进程临时需要做一些不方便的任务的情况下,可以启动一个utility进程来代替主进程执行,主进程与utility进程之间通过IPC(Internet Process Connection)消息来通信。
其他进程
UI进程、存储进程、备进程、Audio进程、Video进程、Profile进程等等。
为什么浏览器要多进程
我们假设浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差
同理如果插件崩溃了也会影响整个浏览器
当然多进程还有其它的诸多优势,不过多阐述
浏览器进程有很多,每个进程又有很多线程,都会占用内存
这也意味着内存等资源消耗会很大,有点拿空间换时间的意思
简述渲染进程Renderer(重)
页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,所以我们要重点了解渲染进程
渲染进程是多线程的,我们来看渲染进程的一些常用较为主要的线程
渲染进程Renderer的主要线程
GUI渲染线程
-
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
- 解析html代码(HTML代码本质是字符串)转化为浏览器认识的节点,生成DOM树,也就是DOM Tree
- 解析css,生成CSSOM(CSS规则树)
- 把DOM Tree 和CSSOM结合,生成Rendering Tree(渲染树)
- 当我们修改了一些元素的颜色或者背景色,页面就会重绘(Repaint)
- 当我们修改元素的尺寸,页面就会回流(Reflow)
- 当页面需要Repaing和Reflow时GUI线程执行,绘制页面
- 回流(Reflow)比重绘(Repaint)的成本要高,我们要尽量避免Reflow和Repaint
- GUI渲染线程与JS引擎线程是互斥的
- 当JS引擎执行时GUI线程会被挂起
- GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
JS引擎线程
这个引擎主要由两个部分组成,Memory Heap 和 Call Stack,即内存堆和调用栈。
内存堆:进行内存分配。如变量赋值。
调用栈:这是代码在栈帧中执行的地方。调用栈中顺序执行主线程的代码,当调用栈中为空时,js引擎会去消息队列取消息,取到后就执行。
- JS引擎线程就是JS内核,负责处理Javascript脚本程序(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码
- JS引擎一直等待着任务队列中任务的到来,然后加以处理
- 浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的
- 一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- GUI渲染线程与JS引擎线程是互斥的,js引擎线程会阻塞GUI渲染线程
- 就是我们常遇到的JS执行时间过长,造成页面的渲染不连贯,导致页面渲染加载阻塞(就是加载慢)
- 例如浏览器渲染的时候遇到script标签,就会停止GUI的渲染,然后js引擎线程开始工作,执行里面的js代码,等js执行完毕,js引擎线程停止工作,GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况
事件触发线程
- 属于浏览器而不是JS引擎,用来控制事件循环,并且管理着一个事件队列(task queue)(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当js执行碰到事件绑定和一些异步操作(如setTimeOut,也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等,),会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。
- 主要负责将准备好的事件交给JS引擎线程执行 .比如setTimeout定时器计数结束,ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待JS引擎线程的执行
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
- 因为JS是单线程,所以这些待处理队列中的事件都得排队等待JS引擎处理
定时触发器线程
- setInterval 与 setTimeout 所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的 (因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确)
- 通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待JS引擎空闲后执行),这个线程就是定时触发器线程,也叫定时器线程
- W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中再由JavaScript引擎执行
- 简单说就是当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应 (准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行
执行栈和任务队列
JavaScript 在运行时会将变量存放在堆(heap)和栈(stack)中,堆中通常存放着一些对象,而变量及对象的指针则存放在栈中。JavaScript 在执行时,同步任务会排好队,在主线程上按照顺序执行,前面的执行完了再执行后面的,排队的地方叫执行栈(execution context stack)。JavaScript 对异步任务不会停下来等待,而是将其挂起,继续执行执行栈中的同步任务,当异步任务有返回结果时,异步任务会加入与执行栈不一样的队列,即任务队列(task queue),所以任务队列中存放的是异步任务执行完成后的结果,通常是回调函数。
当执行栈的同步任务已经执行完成,此时主线程闲下来,它便会去查看任务队列是否有任务,如果有,主线程会将最先进入任务队列的任务加入到执行栈中执行,执行栈中的任务执行完了之后,主线程便又去任务队列中查看是否有任务可执行。主线程去任务队列读取任务到执行栈中去执行,这个过程是循环往复的,这便是 Event Loop,事件循环。
执行栈:管理主线程上同步任务
任务队列:管理异步任务
只要执行栈空了,就会去读取任务队列
宏任务和微任务 在这么我们都将其看作是异步任务,因为通俗理解主线程是在执行同步任务完成之后才会来执行宏任务和微任务。在这里我们通俗理解微任务的优先级比宏任务要高,先于宏任务执行。
了解了上面这些基础后,接下来我们开始进入今天的正题,输入URL拿到资源之后,如何渲染,又经历了那些过程?
渲染过程
Webkit渲染流程图
Webkit内核,其前身是KDE小组的KHTML引擎。开始是苹果的Safari浏览器中使用,而后再谷歌的Chrome浏览器也采用,并使Webkit更加深入人心,以致于很多人把Webkit称为chrome内核,(在此为Safari默哀一小会儿)。然而 2013 年 4 月 3 日,谷歌在 Chromium Blog 上发表 博客,称将与苹果的开源浏览器核心 Webkit 分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎(即浏览器核心),内置于 Chrome 浏览器之中。所以现在的谷歌浏览器内核其实应该叫Blink,当其本质还是Webkit,我们可以如此理解:Blink是Webkit的分支,而Webkit又是KHTML的分支。在此需要补充说明一点Opera浏览器已放弃自己的Presto内核,加入谷歌的阵营,先用Webkit再用Blink。
我们都知道,浏览器之间的渲染流程是有些细微差别的,我们这里介绍的一些知识点是基于Chrome的,也就是Webkit,毕竟它是主流,先来看一下Webkit的渲染流程图
解析HTML构建DOM树
浏览器渲染,那么浏览器肯定就拿到页面内容了,肯定要先解析HTML的
如果是第一次看到这张图可能看不懂,没关系,慢慢道来
先来看图中解析DOM的这几个大过程
Bytes(字节) -> Characters(字符) -> Tokens(词) -> Nodes(节点) -> DOM(DOM树)
首先,发起请求拿到页面 HTML 内容,这个内容它是0/1这样的原始 字节流
接着,浏览器拿到这些 HTML 的原始字节,根据文件的指定编码 (例如 UTF-8) 将它们转换成各个 字符
现在字节流变成了 字符流 ,也就是一大串字符串
为了把 字符流 解析成正确的 DOM 结构,浏览器还要继续努力
接着进行 词法解析 ,把字符流初步解析成我们可理解的 词,学名叫 token
什么是词 (Token)
词 是编译原理中的最小单元,如标签开始、属性、标签结束、注释、CDATA节点
Token 会标识出当前 Token 的种类,有点绕,怎么说方便理解呢,举个例子
<div class="haha">haha</div>
如上,这是一个标签它有一个class属性 (废话),但是浏览器拿到的只是字符串,它不知道这都是什么标签有啥属性要做什么,那么得给它一点一点拆开读,就是词法解析,怎么解析,就像下面这样
1. <div # 哦,看到了<div,这是一个div标签的开始
2. class="haha" # 这是一个class属性
3. > # 哦,到这儿是一个完整的div开始标签
4. haha # 嗯,这是一个文本
5. </div> # 奥,看到了</div>,整个div标签结束了
词法解析 是编译原理中的概念,上面是极度简化版本 (防大佬死磕),只是为了方便大家理解
现在理解了吗,Tokens 这个阶段中会标识出当前 Token 是 开始标签 或是 结束标签 亦或是 文本 等信息
那么我们收回思路,接着上面的步骤,经历 词法解析 我们把字符流解析成了 词 (Token)
接着在每个 Token 被生成后,会立刻消耗这个 Token 创建出节点对象,就是 节点 (Nodes) 阶段
把开始结束标签配对、属性赋值好、父子关系这些都连接好了,最终就构成了 DOM 树
后面这两小步也可称为 语法解析 ,到此 DOM Tree 就解析完了
另外多嘴一句,DOM树(DOM Tree) | 文档对象模型 ,这些东西说的都是 DOM树
解析CSS构建CSSOM树
有 HTML 解析,那肯定有 CSS 解析,比如我们构建 DOM 的时候遇到了 link 标记,该标记引用一个外部 CSS 样式表,那么浏览器会认为它需要这个外部样式资源,就会立即发出对该资源的请求,并返回样式内容,也是字节流
与处理 HTML 时一样,将收到的 CSS 规则转换成某种浏览器能够理解和处理的东西,基本步骤重复 HTML 过程,不过是构建 CSS 而不是 HTML
CSS 字节转换成字符,接着词法解析与法解析,最后构成 CSS对象模型(CSSOM) 的树结构
我们都知道,节点样式是可以继承的,所以在构建的过程中浏览器得递归 DOM 树来确定元素到底是什么样式,为了 CSSOM 的完整性,只有等构建完毕才能进入到下一个阶段,所以就算 DOM 已经构建完了,也得等 CSSOM,然后才能进入下一个阶段
所以 CSS 的加载速度与构建 CSSOM 的速度会影响首屏渲染速度,这就是我们常说的 CSS 资源的加载会阻塞渲染
怎么优化?DOM树要小,CSS尽量用 id 和 class 少直接用标签
解析JavaScript脚本
这个解析 JS 的步骤是不固定的,因为在构建DOM 树的过程中,当 HTML 解析器遇到一个 script 标记时,即遇到了js,立即阻塞DOM树的构建,就会将控制权移交给 JavaScript 引擎,等到 JavaScript 引擎运行完毕,浏览器才会从中断的地方恢复DOM树的构建
为什么上面也说了,JS会对DOM节点进行操作,浏览器无法预测未来的DOM节点的具体内容,为了防止无效操作,节省资源,只能阻塞DOM树的构建
例如,若不阻塞DOM树的构建,若 JS 删除了某个DOM节点A,那么浏览器为构建此节点A花费的资源就是无效的
若在 HTML 头部加载 JS 文件,由于 JS 阻塞,会推迟页面的首绘,所以为了加快页面渲染,一般将 JS 文件放到HTML 底部进行加载,或是对 JS 文件执行 async 或 defer 加载
- async 是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在 onload 前,但不确定在 DOMContentLoaded 事件的前或后
- defer 是延迟执行,在浏览器看起来的效果像是将脚本放在了 body 后面一样(虽然按规范应该是在 DOMContentLoaded 事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)
CSS加载对HTML,jS影响
- css加载不会阻塞DOM树的解析
- css加载会阻塞DOM树的渲染
- css加载会阻塞后面js语句的执行、
https://www.cnblogs.com/caijinghong/p/13994813.html
构建渲染树/呈现树(Render Tree)
渲染树 ( Render Tree ) 由 DOM树、CSSOM树 合并而成,但并不是必须等 DOM树 及 CSSOM树 加载完成后才开始合并构建 渲染树,三者的构建并无先后条件,也并非完全独立,而是会有交叉,并行构建,因此会形成一边加载,一边解析,一边渲染的工作现象
CSSOM 树和 DOM 树 合并成渲染树,渲染树 只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上
如上图 ,为了构建渲染树,我们看看浏览器都做了什么
- 浏览器首先会从DOM树的根节点开始遍历每个可见节点
- 例如脚本标记、元标记等有些节点不可见,因为它们不会体现在渲染输出中,所以会被忽略
- 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如上图的其中一个 span 标签有 display: none 属性,也会被忽略
- 对于每个可见节点,找到其对应的的 CSSOM 规则并应用它们
- 输出可见节点,连同其内容和计算的样式
布局(Layout)
渲染树 同时包含了屏幕上的所有可见内容及其样式信息,有了渲染树,再接着就要进入布局 ( layout ) 阶段了,到目前为止,我们计算了哪些节点应该是可见的以及它们的计算样式,但我们还没有计算它们在设备 视口 内的确切位置和大小,这就是 布局 ( Layout ) 阶段,也称为 自动重排 或 回流 ( Reflow )
此阶段一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
简单举个例子,我们看下面这段代码
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>hahaha</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world</div>
</div>
</body>
</html>
上面代码网页的正文包含两个嵌套 div:第一个父 div 将节点的显示尺寸设置为视口宽度的 50%,父 div 包含的第二个 div 将其宽度设置为其父项的 50%,即视口宽度的 25% (网图侵删)
布局流程的输出是一个 盒模型,它会精确地捕获每个元素在视口内的确切位置和尺寸,当然,所有相对测量值都转换为屏幕上的绝对像素
我们先往下看,稍后还会给大家介绍
绘制(Painting)
经历了以上种种步骤,终于来到了 绘制 ,这一步听名字就能想到其作用了
经由前几步我们知道了哪些节点可见、它们的计算样式以及几何信息,我们将这些信息传递给最后一个阶段将渲染树中的每个节点转换成屏幕上的实际像素,也就是俗称的 绘制 或 栅格化
绘制 过程中有一种绘制叫 重绘,也就是下我们要说的
重绘(Repaint)
元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了,这叫做 重绘 ( Repaint )
回流 (Reflow)
上面我们已经说过了 回流 ,当然也叫 重排 ,要知道,回流 一定伴随着 重绘 ,重绘 却可以单独出现,对比来看,显然回流的成本开销要高于重绘,而且一个节点的回流往往还会导致子节点以及同级节点的回流,所以优化方案中一般都包括,尽量避免 回流
什么会引起回流
- 页面渲染初始化
- DOM结构改变,比如删除了某个节点
- render树变化,比如减少了padding
- 窗口 resize
- 某些 JS 属性,引发回流,很多浏览器会对回流做优化,等到数量足够时做一次批处理回流, 但除了 render树 的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效
- offset ( Top/Left/Width/Height )
- scroll ( Top/Left/Width/Height )
- cilent ( Top/Left/Width/Height )
- width, height
- 调用了 getComputedStyle() 或者IE的 currentStyle
如何减少和避免回流重绘
上面我们说到,回流开销太大了,那么我们肯定是要优化的,接着看,其实就是尽量避免上面那些操作
- 减少逐项更改样式,最好一次性更改 style,或者将样式定义为 class 并一次性更新
- 避免循环操作DOM,让DOM离线后再修改
- 创建一个 documentFragment ,在它上面应用所有DOM操作,最后再把它添加到 window.document
- 先把DOM节点 display:none ( 会触发一次 reflow),然后做修改后,再把它显示出来
- 克隆一个DOM节点在内存里,修改之后,与在线的节点相替换
- 避免多次读取offset等属性,无法避免则将它们缓存到变量
- 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
- 改变字体大小也会引发回流,所以尽可能减少这种操作
- table布局,一个小改动会造成整个table的重新布局,所以,少用为好
总之,说来说去,回流重绘,特别是回流,特别耗费资源,尽量避免就好,关于一些CSS属性会引起的回流重绘,可以去这个网站查查看
合成(Composite)
终于来到了最后一个点 合成 ,我们先来总结一下上面的步骤,到目前我们经历渲染过程如下
- 首先解析 HTML 文档,形成 DOM 树
- 接着解析 CSS,产生 CSSOM树
- 在DOM和CSSOM树解析过程中,遇到 JS,会立即阻塞DOM树的构建,JS解析完成,接着走上面两步
- 再接着,浏览器通过DOM和CSSOM树构建渲染树 ( Render树 )
- 这个过程中,DOM中不可见标签元素不会放到渲染树中,就像 或 display:none
- CSSOM树规则会附加给渲染树的每个元素上
- 渲染树构建完成,浏览器会对这些元素进行定位和布局,这一步也叫 重排/回流 ( Reflow) 或 布局(Layout )
- 接下来绘制这些元素的样式,颜色,背景,大小及边框等,这一步也叫做 重绘 (Repaint)
- 再接下来是我们这最后一步合成( composite ),浏览器会将各层信息发送给GPU,GPU将各层合成,显示在屏幕上
关于合成这一步骤,准备细聊一下子,让大家对其有个基本概念,因为刚开始忽略了它
首先,我们需要简单了解一些基本概念
浏览器渲染方式
浏览器在渲染图形的时候,有一个绘图上下文,绘图上下文又分成两种类型
- 第一种是用来绘制2D图形的上下文,称之为2D绘图上下文(GraphicsContext)
- 第二种是绘制3D图形的上下文,称之为3D绘图上下文(GraphicsContext3D)
网页也有三种渲染方式
- 软件渲染(CPU内存)
- 使用软件绘图的合成化渲染(GPU内存)CSS3D、WebGL
- 硬件加速的合成化渲染(GPU内存)
当然,这些我们也不需要深入理解,知道它们的存在即可
软件渲染技术
Webkit 在不需要硬件加速内容的时候(包括但不限于 CSS3 3D变形、CSS3 3D变换 、 WebGL 和 视频),它就可以使用 软件渲染技术 来完成页面绘制
上面我们看到了软件渲染技术,它是什么呢?我们接着看
对于每个渲染对象,需要三个阶段绘制自己
- 第一阶段是绘制该层中所有块的背景和边框
- 第二阶段是绘制浮动内容
- 第三阶段是前景 ( Foreground ) ,也就是内容部分、轮廓、字体颜色、大小等 ( 内嵌元素的背景、边框等发生在这一阶段 )
硬件加速技术
硬件加速技术是指使用 GPU 的硬件能力来帮助渲染网页 ( GPU的作用主要是用来绘制3D图形并且性能很 nice )
普通图层和复合图层
浏览器渲染的图层一般包含两大类:渲染图层(普通图层)以及复合图层
渲染图层:是页面普通的文档流。无论添加多少元素,还在在同一个默认复合层。
虽然绝对定位(absolute),相对定位(fixed),浮动定位(float)会让元素成为脱离文档流,但它仍然属于默认复合层,共用同一个绘图上下文对象(GraphicsContext)
复合图层,又称图形层。它会单独分配系统资源,每个复合图层都有一个独立的GraphicsContext。(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流Reflow重绘Repaint)
普通文档流大家就可以理解为一个复合图层,我们叫它默认复合层,因为里面不管添加多少元素,其实都是在同一个复合图层中,absolute 布局、 fixed 也一样,虽然可以脱离普通文档流,但它仍然属于 默认复合层
复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能,但也不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡,因小失大
GPU中,各个复合图层是单独绘制的,所以也互不影响,通过 硬件加速 的方式,会声明一个 新的复合图层 ,它会单独分配资源,当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响 默认复合层 里的回流重绘
何为复合图层/硬件加速
复合图层或者说硬件加速,其实就是仅触发合成 composite ,那么也就必须符合以下三个条件
- 不影响文档流
- 不依赖文档流
- 不会造成重绘
寻思一下,可以做到这种情况得还真的不多 ( Chrome )
- 最常用的方式是 transform
- opacity 属性 / 过渡动画 (需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
- will-chang 属性 (这个比较偏僻),一般配合 opacity 与 translate 使用,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层,作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作 (最好用完后就释放)
- 等元素
- 还有以前的 flash 插件等等
通俗一点,假如我们给一个元素加了 transform 属性吧,那么该元素就不会影响也不会依赖文档流,也不会造成重绘,就变成了一个复合图层,也就可以说我们对它使用了传说中的 硬件加速技术
absolute?
到了这里,大家可能有些迷惑,我们不是常说 absolute 是脱离文档流吗,为什么上面复合图层或者说硬件加速中没有 absolute 呢
其实,absolute 虽然可以脱离普通文档流,但是无法脱离默认复合层,就像它的 left 属性可以使用百分比的值,依赖于它的 offset parent
所以,就算 absolute 中信息改变时不会改变普通文档流中的 渲染树 ,但浏览器最终绘制时,是整个复合层绘制的,所以 absolute 中信息改变,仍会影响整个复合层的绘制,浏览器还是会重绘它,如果复合层中内容多,absolute 带来的绘制信息变化过大,资源消耗也非常严重
而我们上面说的硬件加速,那直接就是在另一个复合层了,所以它的信息改变不会影响默认复合层,当然内部肯定会影响属于自己的复合层,仅仅是引发最后的合成渲染
页面渲染优化
浏览器对上文介绍的关键渲染步骤进行了很多优化,针对每一次变化产生尽量少的操作,还有优化判断重新绘制或布局的方式等等,据上文所述,总结下页面渲染这块的优化实践,不分先后,大家也可一块来补充
- HTML文档结构层次尽量少,最好不深于六层
- JS 脚本尽量后放
- 样式结构层次尽量简单
- 少量首屏样式使用内联方式放在标签内
- 在脚本中尽量减少DOM操作,尽量访问离线DOM样式信息,避免过度触发回流
- 减少通过 JS 代码修改元素样式,尽量使用修改 class 名方式操作样式或动画
- 尽量减少浏览器重排和重绘的一些情况发生
- 2020年了!就不要使用 table 布局了
- CSS 动画中尽量只使用 transform 和 opacity ,不会发生重排和重绘
- 隐藏在屏幕外,或在页面滚动时,尽量停止动画
- 尽可能只使用 CSS 做动画,CSS动画肯定比 JS 动画要好很多
- 避免浏览器的隐式合成
- 改变复合层的尺寸
最后
发起一个请求,我们拿到了页面,下载完的网页将被交给浏览器内核(渲染进程)进行处理
- 首先,根据顶部定义的DTD类型进行对应的解析方式
- 渲染进程内部是多线程的,网页的解析将会被交给内部的GUI渲染线程处理
- 渲染线程中的HTML解释器,将HTML网页和资源从字节流解释转换成字符流
- 再通过词法分析器将字符流解释成词
- 之后经过语法分析器根据词构建成节点,最后通过这些节点组建一个DOM树
- 这个过程中,如果遇到的DOM节点是 JS 代码,就会调用 JS引擎 对 JS代码进行解释执行,此时由 JS引擎 和 GUI渲染线程 的互斥,GUI渲染线程 就会被挂起,渲染过程停止,如果 JS 代码的运行中对DOM树进行了修改,那么DOM的构建需要从新开始
- 如果节点需要依赖其他资源,图片/CSS等等,就会调用网络模块的资源加载器来加载它们,它们是异步的,不会阻塞当前DOM树的构建
- 如果遇到的是 JS 资源URL(没有标记异步),则需要停止当前DOM的构建,直到 JS 的资源加载并被 JS引擎 执行后才继续构建DOM
- 对于CSS,CSS解释器会将CSS文件解释成内部表示结构,生成CSS规则树
- 然后合并CSS规则树和DOM树,生成 Render渲染树,也叫呈现树
- 最后对 Render树进行布局和绘制,并将结果通过IO线程传递给浏览器控制进程进行显示