浏览器学习笔记

182 阅读9分钟

1. 浏览器的主要组件

用户界面(所看到的除了用于显示请求页面的主窗口之外的其他部分)、浏览器引擎(用来在显示UI和渲染引擎之间封送动作,负责管理渲染引擎和显示UI)、渲染引擎(用来解析请求的内容)、网络(用来完成网络调用)、UI后端(用来绘制类似组合选择框及对话框等基础组件,底层使用操作系统的用户接口)、JS引擎(用来解释执行JS代码)、数据存储(持久层,存储各类数据,例如cookie形式的webStorage和数据库形式的webDatabase)。

2. 渲染流程

Loader:负责处理所有的HTTP请求以及网络资源的缓存,相当于是从URL输入到Page Resource输出的变换过程。HTML页面中通常存在其他的外链资源,为了不阻塞后续解析过程,一般会有两个IO管道同时存在,一个负责主页面下载,一个负责外链资源的下载。虽然大部分情况下不同资源可以并发下载异步解析,但JS脚本可能会有改变页面的操作,因此有时保持执行顺序和下载管道后续处理的阻塞是不可避免的。

Parser:(1:解析HTML,通过词法解析将HTML文本分割成Token序列,去除无关字符和空格,通过语法解析匹配Token序列生成HTML语法树,将语法树映射为DOM树;(2:解析CSS,页面中的所有CSS由样式表CSS StyleSheet集合(一系列CSS Rule的集合)构成,每一条CSS Rule由选择器CSS StyleSelector部分和声明CSS StyleDeclaration(CSS属性和值的Key-Value集合)部分构成。CSS解析完成后会进行CSS Rule的匹配,即寻找满足每条CSS规则Selector部分的HTML元素,然后将其Declaration部分应用于该元素,匹配过程会考虑优先级等因素;(3:解析JS,JS由JS引擎解析执行,它的作用通常是动态地改变DOM树,即根据时间和事件将一棵DOM树映射为另一棵DOM树。经过Parser模块处理后,将页面文本转换成一棵节点带有CSS Style且会响应事件的Styled DOM树。

Layout:(1:创建渲染树,渲染树和DOM树大体能一一对应,两者在内核中同时存在但作用不同,DOM树是HTML文档的对象表示,同时也作为JS操作HTML的对象接口。Render树是DOM树的排版表示,用以计算可视DOM节点的布局信息和后续阶段的绘制显示。不可视节点没有对应的Render树节点(如head标签);(2:计算布局,HTML采用流式布局模型,页面元素在顺序遍历过程中依次按从左至右、从上至下的排列方式确定各自的位置区域。简单情况下,布局可以顺序遍历一次Render树完成,但当祖先元素依赖后代元素或互相依赖时,需要进行迭代。经过Layout模块的处理后,将带样式的DOM树变换为包含布局信息和绘制信息的Render树。

Paint:负责将Render树映射成可视的图形,它会遍历Render树,调用每个节点的绘制方法将其内容显示在一块画布或者位图上,并最终呈现在浏览器应用窗口中,成为用户看到的实际页面。通常情况下,布局和绘制是相对耗时的操作,一般浏览器会实现一种增量布局和绘制的方式,当一个节点发生变化时,内核会确定其影响范围,在布局阶段标记受该节点布局影响的其他节点,在绘制阶段会标记一个Dirty区域并通知系统重绘。

3. 资源加载

Loader有两条资源加载路径,主资源加载路径和派生资源加载路径。在地址栏输入新地址或在已经打开的页面中点击链接,会触发主资源的加载流程。主资源的加载是立刻发起的,派生资源可能会为了优化网络在队列中等待;主资源加载路径加载失败会有报错提示,派生资源加载路径一般只显示一个占位。

4. 缓存

Page Cache(页面缓存,将浏览的页面状态临时保存在缓存中,以加速页面前进、后退等操作。但若触发加载的原因是地址栏或链接,则页面仍通过网络加载);Memory Cache(内存缓存,浏览器内部的缓存机制,缓存相同URL的派生资源);Disk Cache(磁盘缓存,即强缓存和协商缓存,浏览器将下载的资源保存在本地磁盘,当下次请求相同资源时,直接从本地磁盘中取出资源即可)。

5. 硬件加速

GPU分层渲染。

6. 关键渲染路径(CRP)

构建DOM树、构建CSSOM树、运行JS、创建渲染树、生成布局、绘画。

7. WebKit(浏览器内核)

WebKit嵌入式API(提供给浏览器调用,不同浏览器实现存在差异)、webCore(解析HTML、CSS、DOM、渲染树等,最核心的渲染引擎)、JS引擎(默认JSCore,Chrome选用V8)、WebKit Ports(由于操作系统差异,该部分为可移植部分,主要涉及网络,视频、图片、音频解码等功能)。

8. 操作DOM

由于渲染DOM和操作JS是两个引擎,当通过JS操作DOM时,JS引擎通过调用桥接方法来获取访问DOM的能力,两个引擎通信会带来一些性能损失,这也是频繁操作DOM导致性能低下的部分原因。

9. V8引擎

Loading:Cold Load(首次加载时无数据缓存)、Warm Load(使用了相同的脚本文件,会将编译后的代码和脚本文件一起缓存到磁盘中)、Hot Load(当第三次加载相同脚本,会从磁盘中载入脚本,并拿到上次编译后的代码)。

Parsing:将JS脚本转换成AST的过程,包括词法分析、语法分析。

Interpret:解释阶段,使用Ignition解释器,将AST转换成字节码,字节码相比于机器码,与CPU系统无关、缓存时内存占用小。

Compile:编译阶段,使用TurboFan编译器,将字节码转换成机器码。字节码配合解释器和编译器这一技术设计,称为JIT(即时编译技术),在解释阶段会收集代码信息,标记一些热点代码。编译器会将热点代码直接编译成机器码并缓存,加速执行速度。

Execution:执行阶段,JS作为动态语言,对象属性存在不确定性,每次查找会浪费时间,V8会将首次的分析结果进行缓存,再次访问相同属性时,优先从缓存中取。同时加入了Object Shapes(隐藏类),为每个对象创建一个隐藏类,记录对象的基本信息,包括所有属性以及每个属性相对于对象的地址偏移量。尽量创建“形状一致”的对象,尽量给对象加入临时属性,尽量不要删除对象属性。

10. 内存管理

栈内存和堆内存。堆内存又包含New Spacce(新生代,用于存放新对象)、Old Space(老生代,经过多次GC仍在新生代中存在的对象,空间大小由initial_old_space_size初始值和max_old_space_size最大值控制。old pointer space:存放存活下来包含指向其他对象指针的对象,old data space:存放仅保存数据的对象)、Large Object Space(大对象区,存放大对象,不会被垃圾回收)、Code Space(代码区,存放JIT编译器编译的代码块,唯一可执行代码的空间)、Cell Space(单元区)、Property Cell Space(属性单元区)、Map Space(map区,用于存放对象的隐藏类信息)。

11. GC(垃圾回收)

ECMAScript并未对GC做相关要求,GC完全依赖于底层引擎的能力。判断对象是否活跃的方法:引用计数法、可访问性分析法(V8采用,从GC Roots(浏览器环境包括Window对象、原生DOM集合等)出发,可以遍历到的为活跃对象)。V8基于代际假说(大部分对象在内存中存活时间很短、不死的对象会活的更久)将GC算法分为两种,(1:Major GC,主要使用Mark-Sweep和Mark-Compact算法,针对堆内存的老生代进行GC。标记-清除:从GC Root开始遍历,进行标记清除;标记-整理:清除后产生大量不连续的内存碎片,将存活的对象向一端移动,整理出连续空间。(2:Minor GC,主要使用Scavenger算法,针对堆内存的新生代进行GC。将New Space分为两个区域from-space和to-space,在from-space中标记活跃对象和非活跃对象,将活跃对象拷贝到to-space中,非活跃对象进行清除,交换两块地址的角色。晋升机制:经过一次Scavenger算法后,仍未被标记清除的对象;进行复制的对象大于to-space空间的25%。

12. GC优化策略

由于JS运行在主线程上,一旦执行GC算法,都需要将正在执行的JS脚本暂停,待GC完成后再恢复脚本执行,这种造成系统周期性卡顿的行为叫做全停顿(STW)。优化策略:(1:并行回收,主线程执行一次完整的GC时间较长,开启多个辅助线程来并行处理。V8中的并发机制相对复杂,简化来看,当主线程运行代码时,辅助线程并发进行标记,当标记完成后,主线程和辅助线程并行执行清理;(2:增量回收,并行策略仍是STW,如果老生代中存在大对象,处理依然耗时。V8的垃圾回收器Orinoco增加了增量回收策略,将标记工作分解成小块,插在主线程的不同任务间执行,类似于React Fiber的分片机制。需要满足随时可以暂停和启动(采用三色标记法,最开始所有对象都是白色状态,从GC Root遍历所有可到达对象,标记为灰色,放入待处理队列,从待处理队列中取出灰色对象,将其引用的对象标记为灰色放入待处理队列,自身标记为黑色,重复动作直至灰色待处理队列为空,此时白色对象即为垃圾。被启动时判断有无灰色对象,有则从灰色对象开始执行,无则清理白色对象)、写屏障(一旦有黑色对象引用了白色对象,系统会强制将白色对象标记为灰色对象,从而保证下次GC时状态正确,也称为强三色原则)。