tiantingrui / daily-harvest

记录每日收获
MIT License
2 stars 0 forks source link

浏览器原理 #18

Open tiantingrui opened 2 years ago

tiantingrui commented 2 years ago

浏览器渲染引擎运行机制

这块非常容易作为问答题以及 性能优化的切入点,需重视!

渲染引擎工作流解析

浏览器的渲染引擎承载着把 静态资源转换为可视化界面的任务

网络资源 --> 浏览器 --> 前端界面

渲染引擎处理主要包含以下几个具体流程:

资源文件 --> HTML 解析 --> CSS解析 --> 样式结构合并 --> 布局 --> 绘制

整体来看,这五个过程分别完成了以下任务:

1. HTML解析

在这一步浏览器对HTML文档进行解析,并在解析 HTML 的过程中发出了页面渲染所需的各种外部资源请求。

2. CSS解析

浏览器将识别并加载所有的 CSS 样式信息。

3. 样式与结构合并

将样式信息和文档结构合并,最终生成页面 render 树(:after :before 这样的伪元素会在这个环节被构建到 DOM 树中)。

4. 布局阶段

页面中所有元素的相对位置信息,大小等信息均在这一步得到计算。

5. 页面绘制

在这一步中浏览器会根据我们前面处理出来的结果,把每一个页面图层转换为像素,并对所有的媒体文件进行解码。

这五个步骤可以说是每一步都很关键,每一步都不白干,每一步都有一个阶段性产物作为收获。这些产物是我们理解渲染过程的重要抓手:

阶段性产物分析

这五个流程分别对应了以下五个产物:

  1. DOM 树
  2. CSSOM 树
  3. 渲染树(render tree)
  4. 盒模型(布局渲染树)
  5. 目标界面(绘制)
tiantingrui commented 2 years ago

重绘与重排

重排与重绘——渲染性能问题重要抓手

我们知道,只有经过HTML解析->CSS解析->渲染树构建->布局->绘制这样一个漫长的过程,页面才能够得以呈现在我们面前,不过这还不是故事的全部——在页面初始化完成后,我们可能会通过CSS、JS来对页面中的元素进行修改,这些修改会重新触发页面的一部分生命周期,进而带来性能上的开销。 重走页面生命周期的这个过程,有两种主要的形式——重排与重绘

什么是重排?

当我们的操作引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是重排(也叫回流)。 简而言之,重排多数情况下是由对元素几何属性的修改引发的。 举个简单的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>重排demo</title>
  <style> 
    #target {
      width: 100px;
      height: 100px;
    }
  </style>
</head>
<body>
  <div id="target">
    <span id="targetText">我是一个小测试</span>
  </div>
</body>
</html>
var targetDom = document.getElementById('target');
targetDom.style.width = '200px';

这样的一个修改宽度的动作,不仅影响了盒模型本身的“占地面积”、还会整个页面中其它元素的布局形式。浏览器不得不重新针对布局信息进行计算,这就是典型的重排过程。 当重排发生时,会按如下顺序进行:

构建DOM -> 修改CSSOM -> 更新 渲染树 -> 重新布局 -> 重绘制

什么动作还会触发重排?

注意,重排多数情况下是由对元素几何属性的修改引发的,但不总是由此引发的。以下操作也会触发重排

这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流。

除此之外,当我们调用了 getComputedStyle 方法,或者 IE 里的 currentStyle 时,也会触发回流。原理是一样的,都为求一个“即时性”和“准确性”。

什么是重绘?

当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这个过程叫做重绘。

简而言之,重绘是由对元素绘制属性的修改引发的。

举个例子:

var targetDom = document.getElementById('targetText');
targetDom.style.color = 'red';

浏览器要想改变文字的颜色,只需要去变更像素点的色值即可,不涉及任何布局计算。因此,这是一个典型的重绘过程。

当重绘发生时,“重新来过”的流程如下:

构建DOM -> 修改CSSOM -> 更新渲染树 -> 重绘制 (**这里省下了重布局的操作**)
tiantingrui commented 2 years ago

浏览器原理

前置知识(关于操作系统)

  1. CPU

    中央处理器,可以串行的处理任务

    每一个核心是独立的 cpu单元组

  2. GPU

    图形处理器,每一个GPU的核心只处理一些简单的任务,但是核心数量非常多,也就是说并行计算的能力非常强。

  3. 线程 & 进程

    进程 -> 一个应用程序

    线程 -> 运行在进程里面,一个进程里会有一个或者多个进程

​ 当启动一个程序的时候,操作系统至少会为其创建一个进程,还会为这个进程分配一个独立的内存空间,这块内存存储相关的数据或者状态

​ 当关闭这个应用的时候,操作系统会把对应的进程给kill掉,内存空间也会被释放。

​ 进程与进程之间,是相互独立的

浏览器架构

  1. Browser( 一个 ) - 浏览器进程,负责浏览器的主体部分,包括导航栏,书签,前进后退
  2. Network(一个)- 网络进程,负责页面的网络资源加载
  3. GPU( 一个 ) - 图像渲染进程,负责渲染。一个GPU需要负责打开的tab所有的渲染进程
  4. Renderer(多个) - 渲染进程,负责tab内核网页展示的相关工作,比如将 html、js、css等资源转换为用户可以与之交互并且看得懂的网页,默认情况下,每个tab都有一个独立的渲染进程
  5. Plugin(多个) - 插件进程,每个插件一个进程,flash(已弃用)
  6. Extension(多个) - 扩展程序的进程,每一个扩展程序有一个进程

多进程架构

好处

  1. 容错性

Chrome 会为每一个tab分配一个渲染进程,其中一个标签页挂掉,不会影响其他的标签页

其中一个渲染进程挂掉,不会影响其他的渲染进程

如果所有的tab页都运行在同一个渲染进程里面

  1. 安全性

通过操作系统可以给进程赋予各种权限的原理,浏览器可以让某些进程不具备特定的功能。

比如渲染进程,chrome限制了它对文件读写的能力

  1. 每一个进程可以拥有更多的内存

因为进程在分配的时候,会有一块独立的内存地址空间

坏处

  1. 内存占用的问题

每一个进程都有自己的内存空间

V8引擎,是比较基础的东西,会在每个进程的内存空间中存在

多进程架构的内存优化

默认情况下,每个tab都有一个独立的渲染进程。

chrome的优化是:限制启动的进程数量,当进程的数量到达某个阈值的时候,chrome会将访问同一个网站的tab,都放在同一个渲染进程里。

服务化(性能比较好的设备上)

主要是针对浏览器进程(Browser Process)的优化

高性能的设备上:浏览器进程(Browser Process)会被拆分成各种独立的进程,保证了各种服务端独立性和安全性。

一般的设备上:浏览器进程(Browser Process)

网站隔离

网站隔离会为网站内不同站点的 iframe 分配一个独立的渲染进程 。

浏览器会为每个tab分配一个渲染进程,但是如果一个 tab内只有一个渲染进程,那么它加载的所有 iframe都会共享内存,意味着这种共享内存的情况会破坏掉浏览器的同源策略。

进程之间的内存空间是绝对相互独立的,做网站隔离最好的方式其实就是进程隔离

iframe 如果是同域的话就不会独立分配了

导航的时候发生了什么

浏览器进程(Browser Process),负责各种 tab页之外的东西。

  1. UI线程(UI thread): 绘制浏览器顶部按钮和导航栏输入框等组件,当你在导航栏里输入一些 url 或者关键字的时候,都是 UI 线程在处理你的输入

1. 处理输入

UI线程要做的第一件事情,就是来确定你输入的是什么类型的内容

  1. search words

    会将关键字发送给搜索引擎

  2. url

    直接请求你输入的站点资源

2. 开始导航

输入完后,按下回车。

UI 线程会通知网络进程,初始化一个网络请求来获取站点的内容

这个时候标签页上的 icon 会展示一个圈圈正在旋转,网络进程此时会进行各种DNS解析以及简历TLS连接等操作。

3. 读取响应

  1. 响应类型判断

Response-header:

Content-Type

但是Content-Type 这个响应头不一定存在,浏览器会尝试通过 MIME 类型(就是preview的前几个字符) 嗅探的方式来确定响应类型

  1. 不同响应类型的处理

    1. html , 浏览器会将获取到的响应数据交给渲染进程,由渲染进程进行下一步的工作(常见,重要
    2. zip(文件),响应数据会被交给下载管理器来处理
  2. 安全检查

网络进程在把内容交给渲染进程之前,还会对内容数据做一次安检

如果请求的域名或者响应的内容和某些已知不安全的网站匹配,网络进程会给用户展示一个警告页面,Cross Origin Read Block,保证一些敏感的跨站数据不糊被发送给渲染进程

4. 创建一个渲染进程来绘制页面

在网络进程做完所有检查后,会告诉UI线程所有的数据我这边已经准备好了。

UI线程在收到网络进程的反馈后,会为这个网站来创建一个渲染进程,来渲染页面

chrome在整理有个优化

UI线程在通知网络进程发起网络请求的时候,其实已经知道用户想访问哪个网站了。

UI线程会主动的去寻找或者创建一个渲染进程,节省了新建渲染进程的时间。

tips

如果这个时候有重定向的情况,UI线程会废弃掉刚才预创建的渲染进程,重新给新url创建一个渲染进程。

5. 提交导航

响应数据

渲染进程

都已经准备完毕了。

浏览器进程会通知渲染进程提交本次导航。

渲染进程收到命令,开始提交导航。

提交导航完毕后,渲染进程会通知浏览器进程,导航已经提交了

浏览器进程收到反馈后,认为此次导航结束,开始文档加载阶段。

tab的会话历史,前进按钮,后退按钮也会被更新。

6. 加载完成

当导航提交完成后,渲染进程会开始着手加载资源以及渲染页面。

可以理解为 window.load。渲染进程会通知浏览器进程,然后UI线程停止导航栏上loading的圈圈

如果要导航到不同的站点,会发生什么

无论导航到什么站点,能想到的肯定还是有导航的这些所有步骤

beforeunload

浏览器进程会询问渲染进程:

带有表单的页面,你在想刷新或者跳转到其他页面,或者关掉tab的时候,一般情况下回弹窗一个确认弹窗

为什么浏览器进程需要去问渲染进程,还有没有收尾工作?

因为当前页面发生的一切,包括页面内的js,都是由渲染进程控制的,渲染进程内的js做了什么,浏览器进程根本不知道?

如果重现发起的导航,实在页面内部发起的

比如页面内的js , location.href = xxxx

渲染进程会自己先检查一下有没有 注册 beforeunload 事件,如果没有的话,渲染进程会向浏览器进程发起一次导航请求

如果重新发起的导航,是不同的站点呢?

比如百度调到了谷歌

会另外起一个渲染进程来完成这次导航,当前的渲染进程会 unload

Service Worker场景下的导航

sw 其实只是运行在渲染进程内的一些JS代码。

导航开始的时候,浏览器如何判断当前站点有没有对应的 sw 缓存?

sw在注册的时候,会有一个作用域被记录下来

优化

针对没有注册 sw 的情况,浏览器会执行导航预加载

渲染进程中具体做了什么?

构建DOM树

  1. 构建DOM

    渲染进程会收到HTML数据,主线程会开始解析收到的文本消息,将其转化为dom对象

  2. 子资源加载

    除html以外的资源,图片,js,css等,会从缓存或者网络上获取

  3. js会阻塞HTML的解析过程

    HTML解析器在碰到 script 标签的时候,会停止 html 文档的解析,转向 js 代码的加载 解析执行。

    js 中可能包含 document.write(),这样的代码会改变文档流的形状,也就是改变document,会使整个 DOM 对象发生根本性的辩护

    script 标签 async defer , js 脚本会进行异步加载。

    <link rel="preload"> 会告诉浏览器 当前这个资源肯定会被用到,请求尽早加载这个资源

样式计算

css

主线程会解析页面的 css 从而确定每个 dom 阶段的计算样式, getComputedStyle(),

font-size 10rem --> 100ox;

即使你没有自定义的css,每个dom 节点还是会有自己的初始 css

布局

具体过程

  1. 主线程会遍历刚刚构建的DOM树,根据DOM节点的计算样式计算出一个布居树 layout tree
  2. 布居树上每个节点都会有它在页面上的 xy 坐标,盒子,padding, 具体信息。布局树上只有那些可见的 dom 节点信息

如果一个节点设置了 display: none,这个节点就不会出现在布局树上

visiblity:hidden, 这个节点还是会出现布局树上

所以建议:不要display:none,display:block 切换,影响页面的绘制

绘制

绘制顺序

div z-index 1

div z-index 2

div z-index 0,先绘制这个节点

Service worker 缓存

PWA