关于前端:深入理解浏览器机制

7次阅读

共计 4623 个字符,预计需要花费 12 分钟才能阅读完成。

浏览器中的过程:

  • Browser 过程:是浏览器的主过程,只有一个,负责主控,协调,能够看做是浏览器的大脑

    性能:

    • 负责下载页面的网络文件
    • 负责将 Render 过程失去的存在内存中的位图渲染到页面上
    • 负责创立的销毁 tab 过程(Render 过程)
    • 负责与用户交互
  • GPU 过程:只有一个

    性能:

    • 负责 3D 绘制,只有当该页面应用了硬件加速才会应用它,来渲染页面,否则的话,不实用该过程,二十应用 Browser 过程来渲染页面
  • Render 过程:又名浏览器内核,每个 tab 页面对应一个独立的 Render 过程,外部有多个线程

    性能:

    • 负责脚本执行,位图绘制,事件触发,工作队列轮询等
  • 第三方插件过程:每种类型的插件对应一个过程,仅当应用该插件时才创立

浏览器是多过程的益处非常明显,如果浏览器是单线程的话,则一个页面,一个插件的解体会导致整个浏览器解体,用户体验感会十分差。

浏览器过程间的通信过程

  1. Browser 过程收到用户申请,首先须要获取页面内容(譬如通过网络下载资源),随后将该工作通过 RendererHost 接口传递给 Render 过程
  2. Renderer 过程的 Render 接口收到音讯,简略解释后,交给渲染线程,而后开始渲染
  3. 渲染线程接管申请,加载网页并渲染网页,这其中可能须要 Browser 过程获取资源和须要 GPU 过程来帮忙渲染
  4. 当然可能会有 JS 线程操作 DOM(这样可能会造成回流并重绘)
  5. 最初 Render 过程将后果传递给 Browser 过程
  6. Browser 过程接管到后果并将后果绘制进去

最次要的是浏览器内核:Render 过程(渲染过程)

Render 过程具备多个线程:

js 引擎线程:

  • 也称 js 内核,解析 js 脚本,执行代码;
  • 与 GUI 线程互斥,即当 js 引擎线程运行时,GUI 线程会被挂起,当 js 引擎线程完结运行时,才会持续运行 GUI 线程
  • 由一个主线程和多个 web worker 线程组成,因为 web worker 线程时从属于主线程,无奈操作 dom 等,所以 js 还是单线程语言(在主线程中运行 js 代码)

GUI 渲染线程:

  • 用于解析 html 为 DOM 树,解析 css 为 CSSOM 树,布局 layout,绘制 paint
  • 当页面须要重排 reflow,重绘 repaint 时,应用该线程
  • 与 js 引擎线程互斥

事件触发线程:

  • 当对应事件触发(不论是 Web 接口实现事件触发,还是页面交互事件触发)时,该线程都会将事件对应的回调函数放入 callback queue(工作队列)中,期待 js 引擎线程的解决

定时触发线程:

  • 对应于 setTimeout,setInterval,由该线程来计时,当计时完结,将事件对应的回调函数放入工作队列,期待 js 引擎线程的解决
  • 当定时的工夫小于 4ms 时,一律按 4ms 计算

http 申请线程:

  • 每有一个 http 申请久开一个 http 申请线程
  • 当检测到 http 的状态产生扭转,就会产生一个状态变更事件,如果该事件对应有回调函数的话,则放入工作队列中

工作队列轮询线程:

  • 用于轮询监听工作队列,以晓得工作队列是否为空

Render 过程(渲染过程 / 浏览器内核)中线程间的关系

GUI 渲染线程和 JS 引擎线程互斥

因为 JavaScript 是可操纵 DOM 的,如果在批改这些元素属性同时渲染界面(即 JS 线程和 UI 线程同时运行),那么渲染线程前后取得的元素数据就可能不统一了。

因而为了避免渲染呈现不可预期的后果,浏览器设置 GUI 渲染线程与 JS 引擎为互斥的关系,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新则会被保留在一个队列中等到 JS 引擎线程闲暇时立刻被执行。

JS 阻塞页面加载

从上述的互斥关系,能够推导出,JS 如果执行工夫过长就会阻塞页面。

譬如,假如 JS 引擎正在进行巨量的计算,此时就算 GUI 有更新,也会被保留到队列中,期待 JS 引擎闲暇后执行。而后,因为巨量计算,所以 JS 引擎很可能很久很久后能力闲暇,天然会感觉到巨卡无比。

所以,要尽量避免 JS 执行工夫过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

WebWorker,JS 的多线程

前文中有提到 JS 引擎是单线程的,而且 JS 执行工夫过长会阻塞页面,那么 JS 就真的对 cpu 密集型计算无能为力么?

所以,起初 HTML5 中反对了Web Worker

MDN 的官网解释是:

Web Worker 为 Web 内容在后盾线程中运行脚本提供了一种简略的办法。线程能够执行工作而不烦扰用户界面 一个 worker 是应用一个构造函数创立的一个对象(e.g. Worker()) 运行一个命名的 JavaScript 文件 这个文件蕴含将在工作线程中运行的代码; workers 运行在另一个全局上下文中, 不同于以后的 window 因而,在一个 Worker 内应用 window 快捷方式获取以后全局的范畴 (而不是 self) 将返回谬误。

这样了解下:

  • 创立 Worker 时,JS 引擎向浏览器申请开一个子线程(子线程是浏览器开的,齐全受主线程管制,而且不能操作 DOM)
  • JS 引擎线程与 worker 线程间通过特定的形式通信(postMessage API,须要通过序列化对象来与线程交互特定的数据)

所以,如果有十分耗时的工作,请独自开一个 Worker 线程,这样外面不论如何天翻地覆都不会影响 JS 引擎主线程,只待计算出后果后,将后果通信给主线程即可,perfect!

而且留神下,JS 引擎是单线程的,这一点的实质依然未扭转,Worker 能够了解是浏览器给 JS 引擎开的外挂,专门用来解决那些大量计算问题。

WebWorker 和 SharedWorker

WebWorker 只属于某个页面,不会和其余页面的 Render 过程(浏览器内核过程)共享

  • 所以 Chrome 在 Render 过程中(每一个 Tab 页就是一个 render 过程)创立一个新的线程来运行 Worker 中的 JavaScript 程序。

SharedWorker 是浏览器所有页面共享的,不能采纳与 Worker 同样的形式实现,因为它不隶属于某个 Render 过程,能够为多个 Render 过程共享应用

  • 所以 Chrome 浏览器为 SharedWorker 独自创立一个过程来运行 JavaScript 程序,在浏览器中每个雷同的 JavaScript 只存在一个 SharedWorker 过程,不论它被创立多少次。

看到这里,应该就很容易明确了,实质上就是过程和线程的区别。SharedWorker 由独立的过程治理,WebWorker 只是属于 render 过程下的一个线程。

浏览器的渲染过程

浏览器渲染的过程次要包含以下五步:

  1. 浏览器将获取的 HTML 文档解析成 DOM 树。
  2. 解决 CSS 标记,形成层叠样式表模型 CSSOM(CSS Object Model)。
  3. 将 DOM 和 CSSOM 合并为渲染树(rendering tree),代表一系列将被渲染的对象。
  4. 渲染树的每个元素蕴含的内容都是计算过的,它被称之为布局layout。浏览器应用一种流式解决的办法,只须要一次绘制操作就能够布局所有的元素。
  5. 将渲染树的各个节点绘制到屏幕上,这一步被称为绘制painting

须要留神的是,以上五个步骤并不一定一次性程序实现,比方 DOM 或 CSSOM 被批改时,亦或是哪个过程会反复执行,这样能力计算出哪些像素须要在屏幕上进行从新渲染。而在理论状况中,JavaScript 和 CSS 的某些操作往往会屡次批改 DOM 或者 CSSOM。

渲染过程中的问题

DOMContentLoaded 和 Load

用户看到页面实际上能够分为两个阶段:页面内容加载实现和页面资源加载实现,别离对应于 DOMContentLoadedLoad

  • DOMContentLoaded事件触发时,仅当 DOM 加载实现,不包含样式表,图片等
  • load事件触发时,页面上所有的 DOM,样式表,脚本,图片都已加载实现
  • DOMContentLoaded -> load

渲染阻塞

JS 能够操作 DOM 来批改 DOM 构造,能够操作 CSSOM 来批改节点款式,这就导致了浏览器在遇到 <script> 标签时,DOM 构建将暂停,直至脚本实现执行,而后持续构建 DOM。如果脚本是内部的,会期待脚本下载结束,再持续解析文档。

每次去执行 JavaScript 脚本都会重大地阻塞 DOM 树的构建,如果 JavaScript 脚本还操作了 CSSOM,而正好这个 CSSOM 还没有下载和构建,浏览器甚至会提早脚本执行和构建 DOM,直至实现其 CSSOM 的下载和构建。所以,script标签的地位很重要。

<script src="script.js"></script>

  • 没有 defer 或 async,浏览器会立刻加载并执行指定的脚本,也就是说不期待后续载入的文档元素,读到就加载并执行。

<script defer src="script.js"></script>(提早执行)

  • defer 属性示意提早执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未进行解析,这两个过程是并行的。整个 document 解析结束且 defer-script 也加载实现之后(这两件事件的程序无关),会执行所有由 defer-script 加载的 JavaScript 代码,而后触发 DOMContentLoaded 事件。
  • defer 与相比一般 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析实现之后。

<script async src="script.js"></script> (异步下载)

  • 加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
  • async 属性示意异步执行引入的 JavaScript

浏览器的回流 (reflow) 和重绘(repaint)

HTML 默认是流式布局的,但 CSS 和 JS 会突破这种布局,扭转 DOM 的外观款式以及大小和地位。因而咱们就须要晓得两个概念:replaintreflow

回流(reflow)

当 Render Tree 中局部或全副元素的尺寸、构造、或某些属性产生扭转时,浏览器从新渲染局部或全副文档的过程称为回流。

  • 页面首次渲染
  • 浏览器窗口大小产生扭转
  • 元素尺寸或地位产生扭转
  • 元素内容变动(文字数量或图片大小等等)
  • 元素字体大小变动
  • 增加或者删除可见的 DOM 元素
  • 激活 CSS 伪类(例如::hover)
  • 查问某些属性或调用某些办法

重绘(repaint)

当页面中款式产生扭转 (背景色,色彩,字体扭转(字体大小扭转会产生回流) 等),把这些扭转不会引起页面布局的变动的时候,浏览器就只会把新款式赋予元素并从新绘制,这个过程就叫重绘。

如何防止回流

CSS

  • 防止应用 table 布局。
  • 尽可能在 DOM 树的最末端扭转 class。
  • 防止设置多层内联款式。
  • 将动画成果利用到 position 属性为 absolute 或 fixed 的元素上。
  • 防止应用 CSS 表达式(例如:calc())。

Javascript

  • 防止频繁操作款式, 最好一次性重写 style 属性, 或者将款式列表定义为 class 并一次性更改 class 属性。
  • 防止频繁操作 DOM, 创立一个 documentFragment, 在它下面利用所有 DOM 操作, 最初再把它增加到文档中。
  • 也能够先为元素设置 display: none, 操作完结后再把它显示进去。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 防止频繁读取会引发回流 / 重绘的属性, 如果的确须要屡次应用, 就用一个变量缓存起来。

参考资料:
深刻前端 - 彻底搞懂浏览器运行机制
从浏览器多过程到 JS 单线程,JS 运行机制最全面的一次梳理
浏览器渲染原理与过程

正文完
 0