从0到1了解浏览器原理

Author Avatar
GeniusFunny 1月 16, 2020
  • 在其它设备中阅读本文章

这是一篇讲解浏览器原理的文章,参考后续补上;内容都是自己的笔记

浏览器的多进程架构

chrome

Chrome顶层有1个Browser Process来协调其他的进程:

  • Browser Process
    • 负责地址栏、书签栏,前进后退等部分功能
    • 负责浏览器底层的操作,例如读取文件/网络请求
  • Utility Process
  • GPU Process
    • 负责GPU相关
  • Plugin Process
    • 负责控制一个网页用到的Plugin
  • Render Process
    • 负责一个Tab内关于网页呈现的所有事情【内核】
    • 当打开的tab过多时,后续的tab会共用之前同站点的渲染进程
Site Isolation

允许在同一个tab下的跨站iframe使用单独的渲染进程来处理【安全】

多进程优缺点

优点:

  • 稳定性:某一渲染进程出现问题不会影响整个浏览器
  • 安全性&sandbox:每个渲染进程拥有独立的内存空间,数据并不会共享(例如都包含一个v8)

缺点:

  • 不同进程间的内存不共享,不同进程的内存通常需要包含相同的内容【浪费内存】

浏览器导航流程

从浏览器主页前往某一网站
  • 处理输入
    • 输入query,浏览器利用搜索引擎进行Search Query
    • 输入URL,浏览器导航到指定Site
  • 开始导航:
    • UI线程显示加载圈
    • network线程开始从服务器拉去数据(执行DNS查找、建立TLS等网络I/O)
  • 读取响应
    • 根据响应Content-Type和MIME来执行不同操作
      • 若为text/html,将数据交给渲染进程进行渲染
      • 若为其他媒体类型(如application/pdf),将数据交给Storage线程下载
    • 为什么有了Content-Type,还需要MIME判断?因为Content-Type有可能丢失或错误,所以需要MIME检测
    • 【安全点】,渲染进程会根据数据是否来自可信任的站点决定是否渲染
  • 寻找渲染进程
    • 当network线程下载数据后会通知UI线程已就绪,UI线程会寻找一个渲染进程来渲染数据
    • 优化点】,开始导航时就可以找到一个渲染进程以待渲染数据
  • 完成导航
    • 渲染进程和数据就绪后,渲染进程与浏览器进程IPC获取数据然后渲染页面
    • 当所有onload事件执行完后,渲染进程会发送信号通知浏览器进程渲染完成,浏览器UI线程取消【加载中】
从某页面跳转到另一页面

在开始上述流程时,前页面还会触发beforeunload事件

Service Worker
  • 有些页面还拥有 Service Worker (服务工作线程),Service Worker 让开发者对本地缓存及判断何时从网络上获取信息有了更多的控制权,如果 Service Worker 被设置为从本地 cache 中加载数据,那么就没有必要从网上获取更多数据了。
  • 值得注意的是 service worker 也是运行在渲染进程中的 JS 代码,因此对于拥有 Service Worker 的页面,上述流程有些许的不同。
  • 当有 Service Worker 被注册时,其作用域会被保存,当有导航时,network thread 会在注册过的 Service Worker 的作用域中检查相关域名,如果存在对应的 Service worker,UI thread 会找到一个 renderer process 来处理相关代码,Service Worker 可能会从 cache 中加载数据,从而终止对网络的请求,也可能从网上请求新的数据。
  • 如果 Service Worker 最终决定通过网上获取数据,Browser 进程 和 renderer 进程的交互其实会延后数据的请求时间 。Navigation Preload 是一种与 Service Worker 并行的加速加载资源的机制,服务端通过请求头可以识别这类请求,而做出相应的处理。

参考资源

浏览器内核的多线程架构

浏览器内核是通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。从上面我们可以知道,Chrome浏览器为每个tab页面单独启用进程,因此每个tab网页都有由其独立的渲染引擎实例。一个浏览器内核通常分为多个线程:GUI渲染线程、JavaScript引擎线程、定时器触发线程、事件触发线程、异步HTTP请求线程。

GUI渲染线程

GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”了。

JavaScript引擎线程

​ JavaScript引擎也可以称为JS内核,主要负责处理Javascript脚本程序,例如V8引擎。Javascript引擎线程理所当然是负责解析Javascript脚本,运行代码。

为什么JavaScript是单线程的?

答:JavaScript为处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果JavaScript是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突; 如果Javascript是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript在最初就选择了单线程执行。

GUI 渲染线程 与 JavaScript引擎线程互斥?

答:由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JavaScript引擎为互斥的关系,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。

JS阻塞页面加载?

答:由于GUI渲染线程与JavaScript执行线程是互斥的关系,当浏览器在执行JavaScript程序的时候,GUI渲染线程会被保存在一个队列中,直到JS程序执行完成,才会接着执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

定时器触发线程

​ 浏览器定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

事件触发线程

​ 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

异步HTTP请求线程

​ 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。

深入了解JS Core

原文地址:https://tech.meituan.com/2018/08/23/deep-understanding-of-jscore.html

JS代码被经词法分析、语法分析后生成一棵AST,ByteCodeGenerato根据AST生成字节码,js编译结束后并不会生成可执行文件存储在内存或硬盘,字节码被虚拟机JS Core逐行运行。

JSCore采用的是基于寄存器的指令集架构,有些JVM是基于栈的指令集架构;

基于寄存器的指令集架构执行效率高(操作数不用反复入栈出栈)、内存消耗大、不易于移植(虚拟寄存器需要去匹配到真实机器中CPU的寄存器

img

JS的GC:

img

img

参考资源

浏览器渲染页面流程

img

渲染进程多线程架构:

  • Main Thread:主线程,执行JavaScript代码
  • Worker Threads:worker线程,例如web worker、service worker等
  • Raster Thread:光栅线程
  • Compositor Thread:合成器线程
  • Timeout Thread:定时器线程
  • HTTP Thread:HTTP线程,用于异步请求
  • ….

渲染页面流程:Parse—> Layout —> Paint —> Composite

Parse

Parse阶段将HTML转为DOM树、CSS转为Rule树

Parse分为两阶段:

  1. 词法分析【负责将输入内容分解成一个个有效标记】
  2. 语法分析【根据语言的语法规则分析文档结构,构建语法分析树】
  • 浏览器首先解析HTML文档:解析文档就是将文档转化成为有意义的结果,可以让代码理解和使用的结构,这里解析得到的的结构通常是带变了文档结构的节点树,即DOM树。(具体的过程(编译原理):词法分析将输入内容分解成一个个有效的标记;解析器负责根据语言的语法规则分析文档的结构,从而构建解析树;WebKit使用了创建词法分析器的Flex以及用于创建解析器的Bison。然而解析HTML并不能用常规的自上而下或自下而上的解析器进行解析,原因在于:语言的宽容本质;浏览器历来对一些常见的无效HTML用法采取包容态度;解析过程需要不断反复,源内容在解析过程中通常不会改变,但是在HTML中脚本标记如果包含了document.write就会添加额外的标记。)
  • 解析HTML的算法:标记化、树构建。
    • 标记化是词法分析过程,将输入内容解析成多个标记(起始标记、结束标记、属性名称、属性值),然后传递给树构造器然后接受下一个字符以识别下一个标记,直到整个输入的结束;输出HTML标记
    • 树构建阶段的输入是来自标记化阶段的标记序列,每个标记都会被树构建器处理然后添加到DOM树和开放元素的堆栈中(用于纠正嵌套错误和未闭合的标签)
  • CSS解析:CSS是上下文无关的语法,可以使用常规的各种解析器进行解释。
    • 解析器将CSS文件解析成StyleSheet对象,而且每个对象都包含了CSS规则。
  • 处理脚本和样式表的顺序
    • 脚本:在执行脚本时会阻塞DOM和CSS解析(因为脚本可能修改DOM和CSS),所以需将脚本置于底部或加上下述属性
      • defer:表示并行加载但最后执行脚本,只用于外部脚本
      • async:表示解析HTML其他元素时同时异步加载执行脚本,但是不知道脚本之间执行的顺序(加载完就可执行)
    • 样式表:理论上来说,应用样式表不会更改DOM树,因此似乎没有必要等待样式表并停止文档解析。但是如果脚本在文档解析阶段会请求样式信息,如果当时还没有加载和解析样式表,脚本可能会获得一个错误的回复。

Layout

Layout阶段计算元素的尺寸、样式、位置等

在构建DOM树的同时还会构建Render树:

  • Render树中都是可见元素(不包括dislay:none但包括visibility: hidden
  • 伪元素也被加入到Render树中,尽管其不在DOM树
  • 与此同时,还需要计算每一个呈现对象的可视化属性

样式计算:

位置大小计算:

  • 递归过程,从根元素开始,递归遍历部分或所有的结构并计算大小、位置等信息
  • Dirty位系统,将发生更改的render object标记为dirty,异步触发增量布局
  • 布局的种类
    • 全量布局:触发了整个render tree的重新布局,例如:
      • 全局样式更改
      • 屏幕大小调整
    • 增量布局:对标志为dirty的render object进行重新布局
    • 异步布局:增量布局是异步执行的
      • 类似于任务队列,reflow命令只是被加入队列,具体的执行由调度程序控制
      • 请求样式信息可同步触发增量布局
    • 同步布局:全局布局往往时同步执行

Paint

在绘制阶段,系统会遍历呈现树,并调用呈现元素的paint方法,将元素的内容显示到屏幕上 ,这里同样存在全局绘制增量绘制

Paint阶段决定按什么顺序paint元素

绘制顺序:背景颜色 -> 背景图片 -> 边框 -> 子代 -> 轮廓

Webkit矩形缓存

在重新绘制之前,WebKit 会将原来的矩形另存为一张位图,然后只绘制新旧矩形之间的差异部分。

Composite

img

Node:DOM树中的节点

Layout Object:

  • DOM树中的可视元素,包括伪元素
  • 对象包含尺寸、位置样式等信息

Paint Layer:

  • 在同一坐标空间下的Layout Objects属于同一Paint Layer
  • Paint Layer最开始用于实现层叠上下文用于保证页面元素正确合成
  • Paint Layer类型:
    • NormalPaintLayer(根元素、具有定位、透明、滤镜filter、mask属性、transform属性、)
    • OverflowClipPaintLayer(overflow 不为 visible)
    • NoPaintLayer(不需要paint的paint layer)
    • 满足上面条件的拥有独立的Paint Layer,若不满足则与第一个拥有Paint Layer的元素共享Layer

Graphic Layers:

  • 某些特殊的Paint Layer会被认为是合成层,其拥有独立的GraphicsLayer;
  • 无独立的GraphicsLayer的渲染层与第一个拥有GraphicsLayer的渲染层共享GraphicsLayer;
  • 每个GraphicsLayer拥有一个GraphicsContext,Context负责输出该层的位图(存储在共享内存中,通过纹理上传至GPU,再由GPU将多个位图合并绘制到屏幕上)
  • 渲染层提升为合成层
    • 直接原因
      • 硬件加速的 iframe 元素、硬件加速的插件
      • video 元素、覆盖在video 元素上的视频控制栏
      • 3D 或者 硬件加速的 2D Canvas 元素
      • 3D transform、backface-visibility 为 hidden
      • 对opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition
      • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)
    • 后代元素原因
    • overlap 重叠原因
    • 优点:
      • 合成层的位图由GPU合成,更快
      • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
      • 对于 transform 和 opacity 效果,不会触发 layout 和 paint

Composite技术是将页面划分成多个layer分别栅格化,然后通过composite线程合并layer进行展示,当屏幕scroll时只需要composite可视区域即可。

流程
  • 当layout和paint顺序确定后,Main Thread将这些信息传递给Compositor Thread
  • Compositor将每个layer划分多个tile,将其交给Raster Thread进行栅格化
  • Raster Thread栅格化tile并存储在GPU内存中
  • Compositor Thread收集栅格化完成的tile信息来创建合成帧(compositor iframe)
  • 合成帧通过IPC传递给浏览器进程
  • 合成帧再被传递给GPU进行绘制到屏幕上
优点

由于合成不需要主线程的参与,因此合成器线程不需要等待样式计算或JS执行,所以给用户的体验更加平滑【广泛应用于动画】

响应输入

流程

  • Browser Process最早发现用户输入,但仅知道事件类型、坐标 ;将这些事件类型和发生的坐标传递给Render Process
  • Render Process通过事件类型和坐标找到对应的事件并触发对应事件监听器的回调

合成器相关

合成器会将带有事件监听器的区域标为“Non-Fast Scrollable Region”。

  • 一旦此区域发生输入,合成器会确保这些事件会传递给Main Thread。(合成器线程与主线程通信,花费时间/空间成本)
  • 发生在此区域外的输入,合成器不等待主线程直接创建新的合成帧
  • 事件冒泡机制可能导致合成器与主线程发生不必要的线程通信
合并优化连续的事件

一般我们屏幕的刷新速率为 60fps,但是某些事件的触发量会不止这个值,出于优化的目的,Chrome 会合并连续的事件(如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延迟到下一帧渲染时候执行。

而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非连续性事件则会立即被触发。

coalesced events

合成器发送输入事件给主线程

当合成器线程发送输入事件给主线程时,主线程首先会进行命中测试(hit test)来查找对应的事件目标,命中测试会基于渲染过程中生成的绘制记录( paint records )查找事件发生坐标下存在的元素。

参考资源