星期三, 四月 22日 2020, 10:30 晚上

  8.8k 字     30 分钟       

「一道面试题」输入 URL 到渲染全面梳理中-页面渲染篇

前置知识

此文是一道面试题,又不仅仅是一道面试题,不同的是这道题分了三篇来说,嗯。。可想而知

接上文,上文我们讲了网络通信的部分,那么该说说页面渲染的流程了,也就是当输入一个 URL 拿到了页面后,浏览器怎么解析,怎么呈现

首先要了解这块内容,需要对下面这些知识点有一个简单认知

  • 线程/进程概念及区别
  • 多线程/多进程概念
  • 浏览器的主要进程
  • 浏览器为什么是多进程
  • 渲染进程 Renderer 的主要线程
    • GUI 渲染线程
    • JS 引擎线程
    • 事件触发线程
    • 定时触发线程
    • 异步 http 请求线程
  • 渲染进程的各个线程之间关系及配合

之前写的一篇帖子 「硬核 JS」一次搞懂 JS 运行机制 - 传送门 里有介绍到,下面我们还是重新来一遍吧,花不了多长时间,全当复习一遍,大家也可以自行去看下来了解了解,当然如果你都清楚的话可以直接去看渲染过程

进程与线程

什么是进程

我们都知道,CPU是计算机的核心,承担所有的计算任务

官方说法,进程CPU资源分配的最小单位

字面意思就是进行中的程序,我将它理解为一个可以独立运行且拥有自己的资源空间的任务程序

进程包括运行中的程序和程序所使用到的内存和系统资源

CPU 可以有很多进程,我们的电脑每打开一个软件就会产生一个或多个 进程 ,为什么电脑运行的软件多就会卡,是因为 CPU 给每个 进程 分配资源空间,但是一个 CPU 一共就那么多资源,分出去越多,越卡,每个进程之间是相互独立的, CPU 在运行一个 进程 时,其他的进程处于非运行状态,CPU 使用 时间片轮转调度算法 来实现同时运行多个进程

什么是线程

线程CPU 调度的最小单位

线程 是建立在 进程 的基础上的一次程序运行单位,通俗点解释 线程 就是程序中的一个执行流,一个 进程 可以有多个 线程

一个 进程 中只有一个执行流称作 单线程 ,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行

一个 进程 中有多个执行流称作 多线程,即在一个程序中可以同时运行多个不同的 线程 来执行不同的任务, 也就是说允许单个程序创建多个并行执行的 线程 来完成各自的任务

进程和线程的区别

进程是操作系统分配资源的最小单位,线程是程序执行的最小单位

一个 进程 由一个或多个 线程 组成,线程 可以理解为是一个进程中代码的不同执行路线

进程 之间相互独立,但同一进程下的各个 线程 间共享程序的内存空间 (包括代码段、数据集、堆等) 及一些进程级的资源 (如打开文件和信号)

调度和切换:线程上下文切换比进程上下文切换要快得多

多进程和多线程

多进程: 多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如大家可以在网易云听歌的同时打开编辑器敲代码,编辑器和网易云的进程之间不会相互干扰

多线程: 多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务

JS 为什么是单线程

JS 的单线程,与它的用途有关,作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题

比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

还有人说 js 还有 Worker 线程,对的,为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程是完 全受主线程控制的,而且不得操作 DOM

所以,这个标准并没有改变 JavaScript 是单线程的本质

了解了进程和线程之后,接下来看看浏览器解析,浏览器之间也是有些许差距的,不过大致是差不多的,下文我们皆用市场占有比例最大的 Chrome 为例

浏览器相关

浏览器是多进程的

作为前端,免不了和浏览器打交道,浏览器是多进程的,拿 Chrome 来说,我们每打开一个 Tab 页就会产生一个进程,我们使用 Chrome 打开很多标签页不关,电脑会越来越卡,不说其他,首先就很耗 CPU

浏览器包含哪些进程

  • Browser 进程
    • 浏览器的主进程(负责协调、主控),该进程只有一个
    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将渲染(Renderer)进程得到的内存中的 Bitmap(位图),绘制到用户界面上
    • 网络资源的管理,下载等
  • 第三方插件进程
    • 每种类型的插件对应一个进程,当使用该插件时才创建
  • GPU 进程
    • 该进程也只有一个,用于 3D/动画绘制等等
  • 渲染进程(重)
    • 即通常所说的浏览器内核(Renderer 进程,内部是多线程)
    • 每个 Tab 页面都有一个渲染进程,互不影响
    • 主要作用为页面渲染,脚本执行,事件处理等

为什么浏览器要多进程

我们假设浏览器是单进程,那么某个 Tab 页崩溃了,就影响了整个浏览器,体验有多差

同理如果插件崩溃了也会影响整个浏览器

当然多进程还有其它的诸多优势,不过多阐述

浏览器进程有很多,每个进程又有很多线程,都会占用内存

这也意味着内存等资源消耗会很大,有点拿空间换时间的意思

到此可不只是为了让我们理解为何 Chrome 运行时间长了电脑会卡,哈哈,第一个重点来了

简述渲染进程 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 引擎线程
  • JS 引擎线程就是 JS 内核,负责处理 Javascript 脚本程序(例如 V8 引擎)
  • JS 引擎线程负责解析 Javascript 脚本,运行代码
  • JS 引擎一直等待着任务队列中任务的到来,然后加以处理
    • 浏览器同时只能有一个 JS 引擎线程在运行 JS 程序,所以 js 是单线程运行的
    • 一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序
  • GUI 渲染线程与 JS 引擎线程是互斥的,js 引擎线程会阻塞 GUI 渲染线程
    • 就是我们常遇到的 JS 执行时间过长,造成页面的渲染不连贯,导致页面渲染加载阻塞(就是加载慢)
    • 例如浏览器渲染的时候遇到``标签,就会停止 GUI 的渲染,然后 js 引擎线程开始工作,执行里面的 js 代码,等 js 执行完毕,js 引擎线程停止工作,GUI 继续渲染下面的内容。所以如果 js 执行时间太长就会造成页面卡顿的情况
事件触发线程
  • 属于浏览器而不是 JS 引擎,用来控制事件循环,并且管理着一个事件队列(task queue)
  • 当 js 执行碰到事件绑定和一些异步操作(如 setTimeOut,也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待 js 引擎线程空闲时来处理。
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理
  • 因为 JS 是单线程,所以这些待处理队列中的事件都得排队等待 JS 引擎处理
定时触发器线程
  • setIntervalsetTimeout 所在线程
  • 浏览器定时计数器并不是由 JavaScript 引擎计数的 (因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确)
  • 通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待 JS 引擎空闲后执行),这个线程就是定时触发器线程,也叫定时器线程
  • W3C 在 HTML 标准中规定,规定要求setTimeout中低于 4ms 的时间间隔算为 4ms
异步 http 请求线程
  • 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中再由 JavaScript 引擎执行
  • 简单说就是当执行到一个 http 异步请求时,就把异步请求事件添加到异步请求线程,等收到响应 (准确来说应该是 http 状态变化),再把回调函数添加到事件队列,等待 js 引擎线程来执行

了解了上面这些基础后,接下来我们开始进入今天的正题,输入 URL 拿到资源之后,如何渲染,又经历了那些过程?

渲染过程

Webkit 渲染流程图

我们都知道,浏览器之间的渲染流程是有些细微差别的,我们这里介绍的一些知识点是基于 Chrome 的,也就是 Webkit,毕竟它是主流,先来看一下 Webkit 的渲染流程图

大家如果第一次看到这张图,可能会有点迷茫,不知从哪看起,别急,先大致过一眼,我们下面会慢慢介绍,一步步的分析,如果大家完整的阅读完此文,不妨回过头来再看一遍这张图,会清晰很多

解析 HTML 构建 DOM 树

浏览器渲染,那么浏览器肯定就拿到页面内容了,肯定要先解析 HTML 的

话不多说,我们直接来看 HTML 解析的图 ( 网图侵删 )

image-20200408224734313

如果是第一次看到这张图可能看不懂,没关系,慢慢道来

先来看图中解析 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

image-20200408234346575

CSS 字节转换成字符,接着词法解析与法解析,最后构成 CSS对象模型(CSSOM) 的树结构

我们都知道,节点样式是可以继承的,所以在构建的过程中浏览器得递归 DOM 树来确定元素到底是什么样式,为了 CSSOM 的完整性,只有等构建完毕才能进入到下一个阶段,所以就算 DOM 已经构建完了,也得等 CSSOM,然后才能进入下一个阶段

所以 CSS 的加载速度与构建 CSSOM 的速度会影响首屏渲染速度,这就是我们常说的 CSS 资源的加载会阻塞渲染

怎么优化?DOM 树要小,CSS 尽量用 idclass 少直接用标签 😄

解析 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 文件执行 asyncdefer 加载

  • async 是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在 onload 前,但不确定在 DOMContentLoaded 事件的前或后
  • defer 是延迟执行,在浏览器看起来的效果像是将脚本放在了 body 后面一样(虽然按规范应该是在 DOMContentLoaded 事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)

构建渲染树/呈现树(Render Tree)

渲染树 ( Render Tree ) 由 DOM树CSSOM树 合并而成,但并不是必须等 DOM树CSSOM树 加载完成后才开始合并构建 渲染树,三者的构建并无先后条件,也并非完全独立,而是会有交叉,并行构建,因此会形成一边加载,一边解析,一边渲染的工作现象

CSSOM 树DOM 树 合并成渲染树,渲染树 只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上

image-20200418220809467

如上图 ( 网图侵删 ) ,为了构建渲染树,我们看看浏览器都做了什么

  • 浏览器首先会从 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% (网图侵删)

image-20200418223841109

布局流程的输出是一个 盒模型,它会精确地捕获每个元素在视口内的确切位置和尺寸,当然,所有相对测量值都转换为屏幕上的绝对像素

我们先往下看,稍后还会给大家介绍

绘制(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 属性会引起的回流重绘,可以去这个网站查查看 https://csstriggers.com/

合成(Composite)

终于来到了最后一个点 合成 ,我们先来总结一下上面的步骤,到目前我们经历渲染过程如下

  • 首先解析 HTML 文档,形成 DOM 树
  • 接着解析 CSS,产生 CSSOM 树
  • 在 DOM 和 CSSOM 树解析过程中,遇到 JS,会立即阻塞 DOM 树的构建,JS 解析完成,接着走上面两步
  • 再接着,浏览器通过 DOM 和 CSSOM 树构建渲染树 ( Render 树 )
    • 这个过程中,DOM 中不可见标签元素不会放到渲染树中,就像<head></head> 或 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 也一样,虽然可以脱离普通文档流,但它仍然属于 默认复合层

复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能,但也不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡,因小失大

GPU 中,各个复合图层是单独绘制的,所以也互不影响,通过 硬件加速 的方式,会声明一个 新的复合图层 ,它会单独分配资源,当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响 默认复合层 里的回流重绘

何为复合图层/硬件加速

复合图层或者说硬件加速,其实就是仅触发合成 composite ,那么也就必须符合以下三个条件

  • 不影响文档流
  • 不依赖文档流
  • 不会造成重绘

寻思一下,可以做到这种情况得还真的不多 ( Chrome )

  • 最常用的方式是 transform
  • opacity 属性 / 过渡动画 (需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • will-chang 属性 (这个比较偏僻),一般配合 opacitytranslate 使用,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层,作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作 (最好用完后就释放)
  • <canvas> <webgl> 等元素
  • 还有以前的 flash 插件等等

通俗一点,假如我们给一个元素加了 transform 属性吧,那么该元素就不会影响也不会依赖文档流,也不会造成重绘,就变成了一个复合图层,也就可以说我们对它使用了传说中的 硬件加速技术

absolute?

到了这里,大家可能有些迷惑,我们不是常说 absolute 是脱离文档流吗,为什么上面复合图层或者说硬件加速中没有 absolute

其实,absolute 虽然可以脱离普通文档流,但是无法脱离默认复合层,就像它的 left 属性可以使用百分比的值,依赖于它的 offset parent

所以,就算 absolute 中信息改变时不会改变普通文档流中的 渲染树 ,但浏览器最终绘制时,是整个复合层绘制的,所以 absolute 中信息改变,仍会影响整个复合层的绘制,浏览器还是会重绘它,如果复合层中内容多,absolute 带来的绘制信息变化过大,资源消耗也非常严重

而我们上面说的硬件加速,那直接就是在另一个复合层了,所以它的信息改变不会影响默认复合层,当然内部肯定会影响属于自己的复合层,仅仅是引发最后的合成渲染

页面渲染优化

浏览器对上文介绍的关键渲染步骤进行了很多优化,针对每一次变化产生尽量少的操作,还有优化判断重新绘制或布局的方式等等,据上文所述,总结下页面渲染这块的优化实践,不分先后,大家也可一块来补充

  • HTML 文档结构层次尽量少,最好不深于六层

  • JS 脚本尽量后放

  • 样式结构层次尽量简单

  • 少量首屏样式使用内联方式放在标签内

  • 在脚本中尽量减少 DOM 操作,尽量访问离线 DOM 样式信息,避免过度触发回流

  • 减少通过 JS 代码修改元素样式,尽量使用修改 class 名方式操作样式或动画

  • 尽量减少浏览器重排和重绘的一些情况发生

  • 2020 年了!就不要使用 table 布局了

  • CSS 动画中尽量只使用 transformopacity ,不会发生重排和重绘

  • 隐藏在屏幕外,或在页面滚动时,尽量停止动画

  • 尽可能只使用 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 线程传递给浏览器控制进程进行显示

页面渲染篇到此就结束了,又是上万字,好像也没讲太多东西,大家还是只能以庞观的方式去了解,私下想深入的话还是要多看些相关资料,此文也是我看了很多资料输出的,看完本文,再去看资料或深入应该也会容易了解一些吧,这几篇帖子的核心都脱离不了那道经典面试题,那么看到了这里基本的一些知识点都已经给大家阐述过了,可以自己尝试总结一番了,一定要自己总结再看下文总结篇,这样大家也算没白浪费时间

下一文「一道面试题」输入 URL 到渲染全面梳理下-总结篇,待续哦。。

对您有帮助的话,动动小手,点个赞鼓励下吧,当然,个人理解,如有不正,欢迎指出,不胜感激

哦,还有,可以加个好友加下群一起交流噻,公众号【不正经的前端】也欢迎关注呦 😄

参考 ( 参考了很多帖子,贴了三个认为比较好的,推荐大家看一看 )

  1. 从输入 URL 到页面加载的过程?如何由一道题完善自己的前端知识体系

  2. 从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理

  3. 一篇文章说清浏览器解析和 CSS(GPU)动画优化



一道面试题      面试 网络 浏览器

水平有限,欢迎指错!
联系邮箱:214930661@qq.com
本站文章除特别声明外,均采用 CC BY-SA 3.0协议 ,商业转载请联系作者获得授权,非商业转载请注明出处!

 目录

更多精彩,请关注公众号【不正经的前端】
回复【加群】加入技术交流群,回复【资料】获取精选学习资料