发布时间:2023-04-24 文章分类:WEB开发, 电脑百科 投稿人:王小丽 字号: 默认 | | 超大 打印

一文通透从输入URL到页面渲染的全过程----高频面试

一文通透从输入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从输入到页面渲染这整个过程可以分为 网络通信 和 页面渲染 两个方面,一般后端程序猿回答这个问题侧重网络通信就行了,前端页面渲染也得了解。

总体六个流程

说说页面渲染的流程,也就是当输入一个URL拿到了页面后,浏览器怎么解析,怎么呈现?
首先要了解这块内容,需要对下面这些知识点有一个简单认知

进程与线程

什么是进程

我们都知道,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任务

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

浏览器相关

浏览器是多进程的

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

浏览器包含哪些进程

最新的Chrome浏览器包括那些进程?

至少四个:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程和一个渲染进程

关于如何构建 web 浏览器并不存在标准规范,一个浏览器的构建方法可能与另一个迥然不同。不同浏览器的进程/线程架构一般由下图几部分:
一文通透从输入URL到页面渲染的全过程----高频面试

Browser进程

第三方插件进程

GPU进程

渲染进程(重)

网络进程(Network process)

utility进程(Utility Process)
有时候浏览器主进程需要做一些“危险”的事情,比如图片解码、文件解压缩。如果这些“危险”的操作发生了失败,会导致整个主进程发生异常崩溃,这是我们不愿意看到的。因此Chromium设计出了一个utility进程的机制。主进程临时需要做一些不方便的任务的情况下,可以启动一个utility进程来代替主进程执行,主进程与utility进程之间通过IPC(Internet Process Connection)消息来通信。
其他进程
UI进程、存储进程、备进程、Audio进程、Video进程、Profile进程等等。
一文通透从输入URL到页面渲染的全过程----高频面试

为什么浏览器要多进程

我们假设浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差
同理如果插件崩溃了也会影响整个浏览器
当然多进程还有其它的诸多优势,不过多阐述
浏览器进程有很多,每个进程又有很多线程,都会占用内存
这也意味着内存等资源消耗会很大,有点拿空间换时间的意思

简述渲染进程Renderer(重)

页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,所以我们要重点了解渲染进程
渲染进程是多线程的,我们来看渲染进程的一些常用较为主要的线程
一文通透从输入URL到页面渲染的全过程----高频面试

渲染进程Renderer的主要线程

GUI渲染线程

JS引擎线程

这个引擎主要由两个部分组成,Memory Heap 和 Call Stack,即内存堆和调用栈。
内存堆:进行内存分配。如变量赋值。
调用栈:这是代码在栈帧中执行的地方。调用栈中顺序执行主线程的代码,当调用栈中为空时,js引擎会去消息队列取消息,取到后就执行。

事件触发线程

定时触发器线程

异步http请求线程

一文通透从输入URL到页面渲染的全过程----高频面试

执行栈和任务队列

JavaScript 在运行时会将变量存放在堆(heap)和栈(stack)中,堆中通常存放着一些对象,而变量及对象的指针则存放在栈中。JavaScript 在执行时,同步任务会排好队,在主线程上按照顺序执行,前面的执行完了再执行后面的,排队的地方叫执行栈(execution context stack)。JavaScript 对异步任务不会停下来等待,而是将其挂起,继续执行执行栈中的同步任务,当异步任务有返回结果时,异步任务会加入与执行栈不一样的队列,即任务队列(task queue),所以任务队列中存放的是异步任务执行完成后的结果,通常是回调函数。
当执行栈的同步任务已经执行完成,此时主线程闲下来,它便会去查看任务队列是否有任务,如果有,主线程会将最先进入任务队列的任务加入到执行栈中执行,执行栈中的任务执行完了之后,主线程便又去任务队列中查看是否有任务可执行。主线程去任务队列读取任务到执行栈中去执行,这个过程是循环往复的,这便是 Event Loop,事件循环。

执行栈:管理主线程上同步任务
任务队列:管理异步任务
只要执行栈空了,就会去读取任务队列
宏任务和微任务 在这么我们都将其看作是异步任务,因为通俗理解主线程是在执行同步任务完成之后才会来执行宏任务和微任务。在这里我们通俗理解微任务的优先级比宏任务要高,先于宏任务执行。
一文通透从输入URL到页面渲染的全过程----高频面试

一文通透从输入URL到页面渲染的全过程----高频面试

一文通透从输入URL到页面渲染的全过程----高频面试

了解了上面这些基础后,接下来我们开始进入今天的正题,输入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的渲染流程图
一文通透从输入URL到页面渲染的全过程----高频面试

解析HTML构建DOM树

浏览器渲染,那么浏览器肯定就拿到页面内容了,肯定要先解析HTML的
一文通透从输入URL到页面渲染的全过程----高频面试

如果是第一次看到这张图可能看不懂,没关系,慢慢道来
先来看图中解析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

一文通透从输入URL到页面渲染的全过程----高频面试

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 加载

  1. css加载不会阻塞DOM树的解析
  2. css加载会阻塞DOM树的渲染
  3. css加载会阻塞后面js语句的执行、

https://www.cnblogs.com/caijinghong/p/13994813.html

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

渲染树 ( Render Tree ) 由 DOM树、CSSOM树 合并而成,但并不是必须等 DOM树 及 CSSOM树 加载完成后才开始合并构建 渲染树,三者的构建并无先后条件,也并非完全独立,而是会有交叉,并行构建,因此会形成一边加载,一边解析,一边渲染的工作现象
CSSOM 树和 DOM 树 合并成渲染树,渲染树 只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上
一文通透从输入URL到页面渲染的全过程----高频面试

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

布局(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% (网图侵删)
一文通透从输入URL到页面渲染的全过程----高频面试

布局流程的输出是一个 盒模型,它会精确地捕获每个元素在视口内的确切位置和尺寸,当然,所有相对测量值都转换为屏幕上的绝对像素
我们先往下看,稍后还会给大家介绍

绘制(Painting)

经历了以上种种步骤,终于来到了 绘制 ,这一步听名字就能想到其作用了
经由前几步我们知道了哪些节点可见、它们的计算样式以及几何信息,我们将这些信息传递给最后一个阶段将渲染树中的每个节点转换成屏幕上的实际像素,也就是俗称的 绘制 或 栅格化
绘制 过程中有一种绘制叫 重绘,也就是下我们要说的

重绘(Repaint)

元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了,这叫做 重绘 ( Repaint )

回流 (Reflow)

上面我们已经说过了 回流 ,当然也叫 重排 ,要知道,回流 一定伴随着 重绘 ,重绘 却可以单独出现,对比来看,显然回流的成本开销要高于重绘,而且一个节点的回流往往还会导致子节点以及同级节点的回流,所以优化方案中一般都包括,尽量避免 回流

什么会引起回流

如何减少和避免回流重绘

上面我们说到,回流开销太大了,那么我们肯定是要优化的,接着看,其实就是尽量避免上面那些操作

总之,说来说去,回流重绘,特别是回流,特别耗费资源,尽量避免就好,关于一些CSS属性会引起的回流重绘,可以去这个网站查查看

合成(Composite)

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

关于合成这一步骤,准备细聊一下子,让大家对其有个基本概念,因为刚开始忽略了它
首先,我们需要简单了解一些基本概念

浏览器渲染方式

浏览器在渲染图形的时候,有一个绘图上下文,绘图上下文又分成两种类型

网页也有三种渲染方式

当然,这些我们也不需要深入理解,知道它们的存在即可

软件渲染技术

Webkit 在不需要硬件加速内容的时候(包括但不限于 CSS3 3D变形、CSS3 3D变换 、 WebGL 和 视频),它就可以使用 软件渲染技术 来完成页面绘制
上面我们看到了软件渲染技术,它是什么呢?我们接着看
对于每个渲染对象,需要三个阶段绘制自己

硬件加速技术

硬件加速技术是指使用 GPU 的硬件能力来帮助渲染网页 ( GPU的作用主要是用来绘制3D图形并且性能很 nice )

普通图层和复合图层

浏览器渲染的图层一般包含两大类:渲染图层(普通图层)以及复合图层
渲染图层:是页面普通的文档流。无论添加多少元素,还在在同一个默认复合层。
虽然绝对定位(absolute),相对定位(fixed),浮动定位(float)会让元素成为脱离文档流,但它仍然属于默认复合层,共用同一个绘图上下文对象(GraphicsContext)
复合图层,又称图形层。它会单独分配系统资源,每个复合图层都有一个独立的GraphicsContext。(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流Reflow重绘Repaint)
普通文档流大家就可以理解为一个复合图层,我们叫它默认复合层,因为里面不管添加多少元素,其实都是在同一个复合图层中,absolute 布局、 fixed 也一样,虽然可以脱离普通文档流,但它仍然属于 默认复合层
复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能,但也不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡,因小失大
GPU中,各个复合图层是单独绘制的,所以也互不影响,通过 硬件加速 的方式,会声明一个 新的复合图层 ,它会单独分配资源,当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响 默认复合层 里的回流重绘

何为复合图层/硬件加速

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

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

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

absolute?

到了这里,大家可能有些迷惑,我们不是常说 absolute 是脱离文档流吗,为什么上面复合图层或者说硬件加速中没有 absolute 呢
其实,absolute 虽然可以脱离普通文档流,但是无法脱离默认复合层,就像它的 left 属性可以使用百分比的值,依赖于它的 offset parent
所以,就算 absolute 中信息改变时不会改变普通文档流中的 渲染树 ,但浏览器最终绘制时,是整个复合层绘制的,所以 absolute 中信息改变,仍会影响整个复合层的绘制,浏览器还是会重绘它,如果复合层中内容多,absolute 带来的绘制信息变化过大,资源消耗也非常严重
而我们上面说的硬件加速,那直接就是在另一个复合层了,所以它的信息改变不会影响默认复合层,当然内部肯定会影响属于自己的复合层,仅仅是引发最后的合成渲染

页面渲染优化

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

最后

发起一个请求,我们拿到了页面,下载完的网页将被交给浏览器内核(渲染进程)进行处理

一文通透从输入URL到页面渲染的全过程----高频面试