深入理解浏览器机制

浏览器中的进程:

  • 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进程接收到结果并将结果绘制出来

深入理解浏览器机制_第1张图片

最主要的是浏览器内核: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。

深入理解浏览器机制_第2张图片

渲染过程中的问题

DOMContentLoaded和Load

用户看到页面实际上可以分为两个阶段:页面内容加载完成和页面资源加载完成,分别对应于DOMContentLoadedLoad

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

渲染阻塞

JS可以操作DOM来修改DOM结构,可以操作CSSOM来修改节点样式,这就导致了浏览器在遇到

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

(延迟执行)

  • defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
  • defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。

(异步下载)

  • 加载和渲染后续文档元素的过程将和 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运行机制最全面的一次梳理
浏览器渲染原理与过程

你可能感兴趣的