浏览器渲染原理
前言
在我们面试过程中,面试官经常会问到这么一个问题,那就是从在浏览器地址栏中输入URL到页面显示,浏览器到底发生了什么?
- 浏览器内有哪些进程,这些进程都有些什么作用;
- 浏览器地址输入URL后,内部的进程、线程都做了哪些事;
- 我们与浏览器交互时,内部进程是怎么处理这些交互事件的;
浏览器架构
在讲浏览器架构之前,先理解两个概念,进程和线程;
进程是程度的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,线程(thread)是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源;
简单的说呢,进程可以理解成正在执行的应用程序,而线程呢,可以理解成我们应用程序中的代码的执行器。而他们的关系可想而知,线程是跑在进程里面的,一个进程里面可能有一个或者多个线程,而一个线程,只能隶属于一个进程;
大家都知道,浏览器属于一个应用程序,而应用程序的一次执行,可以理解为计算机启动了一个进程,进程启动后,CPU会给该进程分配相应的内存空间,当我们的进程得到了内存之后,就可以使用线程进行资源调度,进而完成我们应用程序的功能;
而在应用程序中,为了满足功能的需要,启动的进程会创建另外的新的进程来处理其他任务,这些创建出来的新的进程拥有全新的独立的内存空间,不能与原来的进程内向内存,如果这些进程之间需要通信,可以通过IPC机制来进行。
很多应用程序都会采取这种多线程的方式来工作,因为进程和线程之间是互相独立的它们“互不影响”,也就是说,当其中一个进程挂掉了之后,不会影响到其他进程的执行,只需要重启挂掉的进程就可以恢复运行。
浏览器的多进程架构
在Chrome中,主要的进程有4个:
- 浏览器进程(Browser Process): 负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。
- 渲染进程(Render Process): 负责一个Tab内的显示相关的工作,也称渲染引擎。
- 插件进程(Plugin Process): 负责控制网页使用到的插件;
- GPU进程(GPU Process): 负责处理整个应用程序的GPU任务;
这四个进程之间的关系是什么呢?
首先,当我们是要浏览一个网页,我们会在浏览器的地址栏里输入URL,这个时候 Browser Process 会向这个URL发送请求,获取这个URL的HTML内容,然后将HTML交给 Render Process,Render Process 解析HTML内容,解析遇到需要请求网络的资源又返回交给 Browser Process 进行加载,同时通知 Browser Process,需要 Plugin Process 加载插件资源,执行插件代码。解析完成后,Renderer Process 计算得到图像帧,并将这些图像帧交给 GPU Process,GPU Process 将其转化为图像显示屏幕。
多进程架构的好处
- 更高的容错性;
- 更高的安全性和沙盒性;
- 更高的响应速度
多进程架构优化
之前的我们说到,Renderer Process 的作用是负责一个Tab内的显示相关的工作,这就意味着,一个Tab,就会有一个Renderer Process,这些进程之前的内存无法进行共享,而不同进程的内存常常需要包含相同的内容。
浏览器的进程模式
为了节省内存,Chrome提供了四种进程模式(Process Models),不同的进程模式会对tab进程做不同的处理。
- Process-per-site-instance:同一个site-instance使用一个进程;
- Process-per-site:同一个site使用一个进程;
- Process-per-tab:每个tab使用一个进程;
- Single process:所有tab公用一个进程
默认模式选择
Process-per-site-instance 兼容了性能与易用性,是一个比较中庸通用的模式
- 相较于Pocess-per-tab: 能够少开很多进程,就意味着更少的内存占用;
- 相较于Process-per-site:能够更好的隔离相同域名下毫无关联的tab,更加安全;
导航过程都发生了什么
前面我们讲了浏览器的多进程架构,讲了多进程架构的各种好处,和Chrome是怎么优化多进程架构的,下面从用户浏览网页这一简单的场景,来深入了解进程和线程是如何呈现我们的网站页面的。
网页加载过程
- 第一步:处理输入
- 第二步:开始导航
- 第三步:读取响应
- 第四步:查找渲染进程
- 第五步:提交导航
- 第六步:初始化加载完成
网页渲染原理
导航过程完成之后,浏览器进程把数据交给了渲染进程,渲染进程负责tab内的所有事情,核心目的就是将HTML/CSS/JS代码,转化为用于可进行交互的web页面。那么渲染进程是如何工作的呢?
渲染进程中,包含线程分别是:
- 一个主线程
- 多个工作线程
- 一个合成器线程
- 多个光栅化线程
- 构建DOM
- 子资源加载
- JavaScript的下载与执行
- 样式计算 - Style calculation
- 布局
- 绘制
- 合成
浏览器对事件的处理
当页面渲染完毕后,TAB内以及显示出了可交互的WEB页面,用于可以进行移动鼠标、点击页面等操作了,而当这些事件发生时候,浏览器是如何处理这些事件的呢?
点击事件从浏览器进程路由到渲染进程
- 渲染进程中合成器线程接收事件
- 查找事件的目标对象
- 浏览器对事件的优化
总结
浏览器的多进程架构,根据不同的功能划分了不同的进程,进程内不同的使命划分了不同的线程,当用户开始浏览网页时候,浏览器进程进行处理输入、开始导航请求数据、请求响应数据,查找新建渲染进程,提交导航,之后渲染又进行了解析HTML构建DOM、构建过程加载子资源、下载并执行JS代码、样式计算、布局、绘制、合成,一步一步地构建出一个可交互的WEB页面,之后浏览器进程又接受页面地交互事件信息,并将其交给渲染进程,渲染进程内主进程进行命中测试,查找目标元素并执行绑定的事件,完成页面的交互。
JavaScript V8引擎
前言
V8是谷歌推出的开源JavaScript引擎,它是用C++编写的,支持 Google Chrome、Chromiu 网络浏览器和NodeJS,它负责与环境交互并生成字节码来运行程序
V8和其他引擎之间最显着的区别是它的即时(JIT)编译器
如何在V8中执行一段JS代码
- 预分析:检查语法错误但不生成AST
- 生成AST:词法/语法分析后,生成抽象语法树,AST为每一行代码定义键值对。初始类型标识符定义AST属于一个程序,然后所有代码行将定义在主体内部,主体是一个对象数组
- 生成字节码:基线编译器将AST转换为字节码
- 生成机器代码:优化编译器将字节码转换为优化的机器代码。另外,在逐行执行字节码的过程中,如果一段代码经常被执行,V8会直接将这段代码转换并保存为机器码,下次执行不需要经过字节码,优化了执行速度。
引用计数和标记清除简介
引用计数:如果一个变量被分配了一个引用类型,那么这个对象的引用次数是+1。如果变量变为另一个值,则对象的引用数为-1,垃圾回收器将回收引用数为0的对象。但是,当对象被循环引用时,引用数永远不会归零,导致无法释放内存。标记清除:垃圾收集器首先标记内存中的所有对象,然后从根节点开始遍历,清除被引用对象和运行环境中对象的标记,剩下的标记对象不可访问,等待回收对象。
V8如何进行垃圾回收
JavaScript引擎中变量的存储位置主要有两个,栈内存和堆内存。
- 栈内存:存放基本类型数据和引用类型数据的内存地址
- 堆内存:存放引用类型数据
对于不同类型的变量,栈内存和堆内存垃圾回收方式不同
- 堆内存的回收:调用栈上下文切换后回收栈内存,比较简单
- 堆内存的回收:V8的堆内存分为新生代内存和老年代内存。新生代内存是临时分配的内存,存在时间短,老年代内存存在时间长。
新一代内存回收机制
新一代内存容量较小,64位系统下之后32M,新生代的记忆分为两部分:From和To。进行垃圾回收时,先扫描From,回收非存活对象,存活对象按顺序赋值到To,然后交换From/To,等待下一次回收
老年代内存回收机制
- Promotion: 如果新生代的变量经过多次回收后仍然存在,则将其放入老年代的内存中
- 标记清除:老年代内存会先遍历所有对象并标记,然后对正在使用活强引用的对象取消标记,回收标记的对象
- 内存碎片整理:将对象移动到内存的一端
为什么JS比C++等语言慢,V8做了哪些优化?
JS的问题
- 动态类型:每次访问属性/查找方法时,都需要先检查类型;另外,动态类型在编译阶段很难优化
- 属性访问:在C++/Java等语言中,方法、属性都是保存在数组中,只能通过数组位移获取,而JS是保存在对象中,每次获取都要进行 hash 查询
针对V8的优化
优化的JIT(即时编译):与C++/Java等编译型语言相比,JS是同时解释和执行的,效率低下。V8优化了这个过程:如果一段代码被执行多次,V8会将这段代码转换成机器码并缓存起来,下次运行时直接使用机器码。
隐藏类:对于C++等语言,只需几条指令就可以通过偏移获取变量信息,而JS需要进行字符串匹配,效率低下。V8借用类和偏移位置的思想,将对象划分为不同的组,即隐藏类。嵌入式缓存:即缓存对象查询的结果。一般的查询过程是:获取隐藏类地址->根据属性名找到偏移值->计算属性地址,嵌入式缓存就是这个过程结果的缓存。
总结
每当讨论JavaScript的工作原理时,都会谈论事件循环、微任务、宏任务和回调队列。然而,所有这些东西都没有在JavaScript中实现。相反,它们是V8引擎的一部分,负责优化JavaScript代码。