用了十几年的浏览器,你却只会浏览
编辑:浏览器知识来源:腾讯技术工程
导语:本文从市面主流的浏览器及相应的内核引擎开始,介绍了Chromium为代表的浏览器架构及Blink内核的功能架构。Chromium为多进程架构,用户从启动运行浏览器后,先后经过页面导航、渲染、资源加载、样式计算、布局、绘制、合成到栅格化,最后完成GPU展示。而页面渲染完成后,浏览器如何响应页面操作事件也进行了深入的介绍。良心推荐!
本文第二至五部分内容根据 Mariko Kosaka 的英文原版《Inside look at modern web browser》(见参考文献),进行翻译、理解、总结提炼、条理化、加入应用示例、进行相关知识补充扩展而来。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150223-c6f1d.jpeg)
一、浏览器概论
浏览器经历了很多年的发展,浏览器引擎也在不停地迭代和演进。从PC时代到移动端,以独立浏览器的形态还是以系统WebView组件内嵌的形态存在,在互联网的生态系统中一直扮演着重要的角色。了解浏览器及其原理可以让我们打开另一个世界。
1. 浏览器引擎
以下是市面留存的主流浏览器的引擎介绍。
1.1 浏览器引擎
- Trident:IE浏览器引擎
- Gecko:Firefox浏览器引擎
- Presto:Opera浏览器引擎
- Webkit:Safari,Google Chrome浏览器引擎。
1) Chromium:基于webkit,08年开始作为Chrome的引擎,Chromium浏览器是Chrome的实验版,实验新特性。
2) Webkit2:2010年随OS X Lion一起面世。WebCore层面实现进程隔离与Google的沙箱设计存在冲突。
3) Blink:基于Webkit2分支,13年谷歌开始作为Chrome 28的引擎集成在Chromium浏览器里。Android的WebView同样基于Webkit2。
1.2 微软浏览器
目前PC场景操作系统仍是windows一统天下,对桌面用户来说,虽然IE的市场份额在下降,但是IE曾经也风光过。IE内核以Trident为主,最新的Edge也兼容了Chromium内核。
Microsoft Edge:内核为:EDGE,Windows 10默认浏览器,不能单独下载安装。兼容Chromium内核,同时保留EDGE内核来兼容企业网站
- Internet Explorer 11:Windows 8.1,引擎Trident 7.0
- Internet Explorer 10:Windows 8默认浏览器,引擎Trident
- Internet Explorer 9
- Internet Explorer 8:Windows 7集成
- Internet Explorer 7:Windows Vista集成,2016年停止支持
- Internet Explorer 6:2014年停止支持
2. 浏览器架构
目前chromium浏览器的架构主要由下以几个部分构成。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150223-b0319.jpeg)
以下为架构的介绍:
- 操作系统:WebKit可以运行在不同的操作系统上,如Chromium浏览器支持Windows、Linux、Android等系统;
- 第三方库:这些库是WebKit运行的基础,包括2D图形库、3D图形库、网络库、存储库、音视频库等;
- WebCore:WebKit加载和渲染网页的基础,是不同浏览器所使用的WebKit中共享的部分,包括HTML解析器、CSS解析器、SVG、布局、渲染树等等;
- JavaScript引擎:JavaScript解析器,WebKit默认的引擎是JavaScriptCore,Google的Blink为V8引擎;
- WebKit Ports:WebKit中的移植部分,包括网络栈、音视频解码、硬件加速等模块,这部分对WebKit的功能和性能影响比较大。
- WebKit嵌入式接口:WebKit对外暴露的接口层,这个接口是提供给浏览器调用的,如给chromium调用,因为接口与具体的移植也有关系,所以中间会有一个WebKit绑定层
JavaScriptCore(用于Safari)
- JavaSript Parser,JSON Parser
- 字节编译器:使用内部字节码格式
- 汇编程序:在运行时使用代码修补 – >它需要可写代码内存
- 数据流图:基于编译时推测优化生成代码的新举措
- 解释器:运行生成的字节码
- Regexp引擎:支持JIT
- 垃圾收集器:标记和扫描
- 运行时:所有JS全局对象(日期,字符串,数字等)
- 调试器,Profiler
WebCore
- 资源加载器:HTML和XML解析器,DOM
- SVG和SMIL
- CSS:分析器,选择器,动画
- 渲染和布局
- 绑定生成器:IDL文件:JSC,V8,ObjC
- HTML5:音频,视频,画布,WebGL,通知功能
- WebInspector
- 平台集成:图形,字体,声音,视频
相关资料
- Blink内核:https://src.chromium.org/viewvc
2.1 多进程架构
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150223-ebee1.jpeg)
图片引自chromium-design-doc
https://www.chromium.org/developers/design-documents/multi-process-architecture
2.1.1 Chromium多进程架构
早期的web浏览器页面行为不当、浏览器错误、浏览器插件错误都会引起整个浏览器或当前运行的选项卡关闭。因此将chromium应用程序放在相互隔离的独立的进程中:
- 单个程序崩溃不会损害其他应用程序
- 不影响操作系统完整性
- 每个用户不能访问其他用户数据(内存保护、访问控制)
2.1.2 架构组成
- UI主进程:页面选项卡、插件进程作为浏览器进程。
- 渲染进程:特定选项卡作为渲染进程(渲染器),使用Blink(Webkit)开源布局引擎解释和布局HTML。
2.1.3 渲染过程管理
- RenderProcess:每个渲染进程都有一个全局对象,管理与父浏览器的通信并维护全局状态
- RenderProcessHost:浏览器为每个渲染进程维护相应的渲染进程宿主,为每个渲染器管理浏览器状态和IPC(IPC已弃用,最新用Mojo)通信
- RenderView:每个渲染进程有一或多个RenderView对象,对应内容选项卡。RenderProcessHost为渲染器的每个视图(RenderView)维护一个RenderViewHost。每个视图用一个ID区分。
2.1.4 运行流程
- 渲染进程共享:开启浏览器新窗口或新选项卡时,创建新的浏览器进程,并创建RenderView。不同页面/iframe可共享同个渲染进程。
- 崩溃监视:浏览器的IPC连接会监视进程句柄,如句柄对应的渲染进程已崩溃,会向标签发送通知,浏览器会显示“悲伤标签”
- 沙箱运行:渲染器在单独的进程中运行,通过沙箱限制其对系统资源(文件、网络、显示、击键)的访问,而须通过父浏览器进程访问
- 内存交回:进程最小化、隐藏的选项卡将其内存自动放入“可用内存”,内存不足时,windows会将该可用内存数据写磁盘,内存被用于更高优先级任务,以提高可见程序的响应速度。
2.1.5 插件扩展
第三方编写的NPAPI插件因存在不稳定,同时需控制对系统资源的访问,在各自独立的进程中运行,与渲染器分开。
插件设计文档:https://www.chromium.org/developers/design-documents/plugin-architecture
2.2 Webkit(Blink)架构
Blink是Web平台的渲染引擎,实现了浏览器选项卡中呈现的内容:
- HTML:实现Web平台规范,HTML规范(DOM、CSS、Web IDL)
- JavaScript:嵌入V8并运行JavaScript
- 网络:从底层网络堆栈请求资源
- 渲染:构建DOM树,计算样式和布局,嵌入合成器并绘制图形
- 通过内容公共Api对外提供公共能力。
2.2.1 Blink的运行流程
多进程架构,有一个浏览器进程和N个沙盒渲染器进程,Blink在沙盒渲染中运行。浏览器选项卡、iframe可共享同个渲染器进程。
沙箱运行:在沙箱中,须通过父浏览器进程来调度使用资源(文件访问、网络、音视频播放、用户配置文件读取(cookie,密码)等。Blink将浏览器进程抽象为一组服务,使用Mojo与服务、浏览器进程交互。
2.2.2 渲染进程中的线程
- 1个主线程:运行JavaScript、DOM、CSS、样式布局计算
- N个工作线程:运行Web Worker,ServiceWorker,Worklet
- 内部线程:Blink和V8会创建几个线程处理web audio,数据库,GC等
跨线程通信:使用PostTask API,不鼓励共享内存编程除非性能原因。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-f3883.jpeg)
2.2.3 Blink的运行和退出
- 运行:任何使用Blink的场景都需调用 BlinkInitializer::Initialize() 初始化
- 退出:渲染器被强制退出,而不会被清理
2.2.4 Blink的项目代码结构
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-b3f33.jpeg)
- platform:低级功能集合,如单片内核、几何、图形工具
- core:core与DOM紧密结合
- web:实现规范中的web平台功能
- modules:包含独立的功能,如web audio,indexed db等。
- bindings / core:大量使用V8 API
- controller:一组使用core、modules的高级库,如devtools。
- 依赖关系:Chromium -> controller -> modules / bindings -> core / bindings -> platform -> 低级单元(base、V8、cc)
2.2.5 platform内部构成
1) WTF:统一编码原语,如WTF::Vector, WTF::HashSet, WTF::HashMap, WTF::String and WTF::AtomicString来代替std:vector 等。
2) 内存管理:a. PartitionAlloc b.Oilpan(Blink GC) c.malloc/free/new/delete
3) 任务调度:为提高渲染引擎的响应,应执行异步。所有任务都应发布到Blink Scheduler任务队列,指定正确类型并设置优先级,以使得能巧妙地安排任务。
4) Page/Frame/Document/ExecutionContext/DOMWindow
分别对应选项卡、iframe、window.document、主线程和工作线程上下文、JavaScript中的窗口对象。
渲染进程中各种数量关系
- 渲染器进程/Page = 1/N
- 页数/帧= 1/M
- 框架/DOMWindow/文档(或ExecutionContext)= 1/1/1 (会随时变化)
5) 进程外iframe
站点隔离:为每个站点创建一个渲染器进程(相同一二级域名)。跨站点由两个渲染器托管。
6) 分离的iframe/文件
doc = iframe.contentDocument iframe.remove() //iframe 与 dom 树分离 doc.createElement('div'); //仍可在分离的框架上运行脚本
左滑可查看完整代码,下同
7) Web IDL绑定
8) V8
- Isolate:一一对应物理线程。主线程、工作线程都有自己的独立线程。
- Context:对应全局对象,如为Frame时对应Frame的窗口对象,每个帧都有自己的窗口对象
- World:支持Chrome扩展程序内容脚本
关系:一个frame = N个窗口对象 = 用于N个world。Context对应该窗口对象
V8的API低级且难以使用,在platform/bindings中提供很多V8 API辅助类。每个C++ DOM对象,如Node都有其对应的V8包装器。V8包装器对应的C++ DOM对象具有强引用。C++ DOM对象只对V8包装器弱引用。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-61f34.jpeg)
2.3 V8
V8是Google的开源高性能JavaScript和WebAssembly引擎,用C++编写,它实现ECMAScript和WebAssembly,可独立运行或嵌入到任何C++应用程序中,如Chrome和Node.js。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-db55b.jpeg)
相关资料ECMAScript:https://tc39.es/ecma262/ WebAssembly:https://webassembly.github.io/spec/core/
二、Chrome的多进程架构
注意:以下内容根据 Mariko Kosaka 的英文原版《Inside look at modern web browser》(见参考文献),进行翻译、理解、总结提炼、条理化、加入应用示例、进行相关知识补充扩展而来。
1. 背景:计算机的核心是CPU和GPU
CPU:Center Processing Unit,同时支持并行、串行操作,需很强通用性处理不同数据类型、要支持复杂通用逻辑判断,需引入大量分支和中断处理,结构异常复杂。
GPU:Graphics Processing Uint,专为执行图形渲染必须的复杂的数学和几何计算而设计。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-e86fd.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-3c1fc.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-e1ace.jpeg)
三层计算机体系结构
图片引自Mariko Kosaka的《Inside look at modern web browser》
2. 基础:在Process和Thread执行程序
启动应用程序时,创建一个进程,并提供”slab”内存,所有应用程序状态保存在该专用内存中,关闭程序时,系统释放内存。
应用程序可能会创建多个线程完成工作任务。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-4c8bf.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-5cae9.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
3. 浏览器架构
浏览器架构没有统一标准规范,不同浏览器可能使用不同线程或多个不同进程来构建web。少数线程间通过IPC通信。
3.1 不同浏览器实现的体系结构
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150224-ce302.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
3.2 Chrome的多进程架构
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-d1a3e.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
4. 不同进程作用
- 浏览器:控制应用程序chrome部分,包括地址栏,书签,后退和前进按钮。及处理Web浏览器的不可见特权部分,例如网络请求和文件访问
- 渲染:控制显示网站的选项卡内的任何内容
- 插件:控制网站使用的任何插件,例如flash。
- GPU:独立于其他进程处理GPU任务。它被分成不同的进程,因为GPU处理来自多个应用程序的请求并将它们绘制在同表面中。
- 其他进程:浏览器右上角更多 -> 更多工具 -> 任务管理器,查看其他进程,如实用程序网络服务、辅助框架
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-9084b.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
5. 多进程架构
优点:
- 防一个页面崩溃影响整个浏览器
- 安全性和沙箱:操作系统提供了限制进程权限的方法,因此浏览器可以从某些功能中对某些进程进行沙箱处理。如任意访问文件
- 进程有自己的私有内存空间,可以拥有更多的内存。为了节省内存,Chrome限制了它可以启动的进程数量。限制因设备的内存和CPU功率而异,但当Chrome达到限制时,它会在一个进程中开始从同一站点运行多个选项卡
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-285c0.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
6. 服务化 – 节省更多内存
浏览器程序中相同的功能方法,正在将浏览器的每个部分作为一项服务运行,可以轻松拆分为不同进程或聚合成一个进程。
当Chrome在强大的硬件上运行时,它可能会将每个服务拆分为不同的流程,从而提供更高的稳定性,但如果它位于资源约束设备上,Chrome会将服务整合到一个流程中,从而节省内存占用。
Android的平台上已经使用了类似的方法来整合流程以减少内存使用。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-df82f.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
7. 给Iframe分配单独渲染进程 – 站点隔离
站点隔离:因不同站点之间共享内存空间会存在同源策略绕过(Meltdown and Spectre)安全问题:https://blog.csdn.net/wlmnzf/article/details/79319509%22%20/t%20%22_blank 。因此为每个跨网站iframe运行单独的渲染器进程。
站点隔离难点:从根本上改变iframe的通信方式,包括ctrl+F查找、打开devtools等需在不同渲染器进程访问。【重大版本】。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-98000.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
三、页面导航过程
1. 浏览器进程运行
多进程架构启动多个进程处理不同的任务。选项卡外部的所有内容都由浏览器进程处理(包含UI线程、网络线程、存储线程)。在地址栏输入url时,由浏览器进程的UI线程处理。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-e98bd.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
2. 处理输入
当用户开始输入地址栏时,UI线程需判断是搜索查询还是URL。
- 查询:发送到搜索引擎
- URL:请求URL的网站
3. 开始导航
用户点击进入时:
- 有注册设置Service Worker从缓存加载页面,渲染进程中运行JavaScript代码,从缓存加载页面,无需请求网络
- 未设置Service Worker时:
1) UI线程启动网络调用以获取站点内容,选项卡加载转圈
2) 网络线程通过DNS查找域名对应IP及建立http连接
3) 网络线程接收处理301重定向头。网络线程与请求重定向的UI线程通信,启动另一个URL请求
Service Worker
Service Worker注册后,保留其范围为参考。当导航时,网络线程根据注册的范围检查域名,若url已注册Service Worker,UI线程找到渲染进程执行ServiceWorker代码,从缓存加载数据或从网络加载新资源。生命周期见:https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
导航预加载
如果ServiceWorker最终决定从网络请求数据,浏览器进程与渲染进程间的往返可能导致延时,通过与ServiceWorker启动并行加载资源加速来减少延时,允许标记这些请求,允许服务器决定为这些请求发送不同的内容。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-21a9e.jpeg)
图片引自上面ServiceWorker的生命周期
4. 读取响应结果
4.1 确定文件MIME类型
网络线程查看流的前几个字节,响应头中Content-Type头确定MIME数据类型。因此数据可能丢失,因此用MIME嗅探方式来查看资源。https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
4.2 处理不同MIME文件
响应文件是HTML,则将数据传递给渲染器进程。如果为.zip或其他文件则将数据传递给下载管理器。
4.3 安全检查
- 恶意名单检查:如果域和响应数据在恶意站点名单中,则网络线程发出和显示警告页面。
- 跨域读取检查:CrossOriginReadBlock检查,敏感的跨站点数据不进入渲染器进程
5. 查找渲染进程
所有检查完成后,网络线程告知UI线程数据已准备就绪,UI线程找到渲染进程以继续渲染网页。
由于网络请求可能需要几百毫秒才能得到响应,为加速此过程,在开始导航网络线程发送url请求时,已经主动进行查找、启动渲染进程,数据接收完成后,渲染进程已备用。
6. 提交导航
现在数据和渲染器进程已准备就绪,IPC将从浏览器进程发送到渲染进程以提交导航。渲染进程确认提交完成,导航完成。文档加载开始。
1、UI更新:地址栏更新、安全指示器、站点设置UI会反映新页面站点信息
2、选项卡的会话历史记录更新(前进/后退),为便于关闭浏览器后恢复,历史记录到磁盘
7. 初始化 load complete
提交导航后,渲染器进程将继续加载资源并呈现页面,一旦渲染器进程“完成”(onload事件在所有帧上触发执行完成后)渲染,它就会将IPC发送回浏览器进程。
UI线程停止选项卡的加载转圈。
8. 导航到其他站点
导航完成后,再次将不同的URL放到地址栏导航,浏览器会检查当前渲染网站的beforeunload事件。如有设置导航或关闭选项卡时发出警报“离开这个网站吗?” 包含JavaScript代码的选项卡内的所有内容都由渲染进程处理。
渲染进程导航操作单击链接或客户端JavaScript已运行window.location = “https://newsite.com“ ,过程与流程器进程启动导航过程相同,不同点在于导航请求是从渲染进程启动到浏览器进程。
页面生命周期:https://developers.google.com/web/updates/2018/07/page-lifecycle-api#overview_of_page_lifecycle_states_and_events
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-ad577.jpeg)
图片引自上面的页面生命周期
四、页面渲染
1. 渲染进程处理页面内容
渲染进程负责选项卡内发生的所有事情。在渲染器进程中
- 主线程:处理您发送给用户的大部分代码。
- 工作线程:处理WebWorker或ServiceWorker
- 排版线程:Compositor
- 栅格线程
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-d0022.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
2. 解析
2.1 构建DOM
当渲染进程接收提交的导航消息和HTML数据,主线程开始解析文本串(HTML),使之成为一个DOM。解析中遇到html能优雅容错。
DOM:浏览器页面内部表示,提供给开发人员通过JS与DOM交互的数据结构和API。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-29043.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
2.2 子资源加载
网站通常使用图像,CSS和JavaScript等外部资源,需要从网络或缓存加载。在解析构建DOM时,主线程可以逐个请求它们。为了加快速度“预加载扫描器”同时运行。
2.3 JavaScript阻塞解析
当遇到<script>时,暂停HTML解析,加载解析执行JS代码。因为JS可能会改变Html的结构导致重新reflow和repaint。
解析模型见:https://html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model
V8相关讨论:https://mathiasbynens.be/notes/shapes-ics
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-e44a0.jpeg)
3. 确定加载资源方式
- 在<script>加async或defer属性,浏览器异步加载和运行JS,不阻止解析。
1) async:指示浏览器尽可能异步加载脚本,默认同步加载脚本(async=false)
2) defer:指示脚本要在解析文档之后但在触发DOMContentLoaded之前执行。
- 用JS模块化
- 样式文件中加rel=preload
- 可设置资源加载优先级,优化加载渲染关键路径资源,优化性能。见:https://developers.google.com/web/fundamentals/performance/resource-prioritization
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150225-df8ed.jpeg)
上面图片引自https://v8.dev/features/modules
4. 样式计算
主线程解析CSS并确定每个DOM节点的计算样式,再根据CSS选择器将哪种样式应用于哪个元素。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-e2e74.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
未提供任何样式时,每个DOM节点都具有默认的Computed样式。见:https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/resources/html.css
5. 布局 – layout
渲染进程知道每个节点的文档结构和样式。布局是查找元素几何的过程。
1) 布局过程
主线程 遍历DOM并计算样式,并创建布局树(layout tree, 包含坐标和边界框大小等信息)。
布局树的特殊情况
- 不包括display: none的节点
- 包括visibility: hidden的元素
- 包括伪类元素,如::before
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-b286b.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
确定页面布局的挑战:
- 从上到下的块流须考虑字体的大小以及在哪里划分它们,最终影响段落的大小和形状,影响下一段的位置
- CSS可以使元素浮动到一侧,屏蔽溢出项,并更改写入方向
6. 绘制 – Paint
知道元素的大小,形状和位置,但是不知道绘制的顺序。主线程遍历布局树以创建绘制记录,绘制记录是绘画过程的一个注释。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-5ca5c.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
渲染中的难点
- 布局树变化:在每个步骤中,前一个操作的结果用于创建新数据。例如,如果布局树中的某些内容发生更改,则需要为文档的受影响部分重新生成“绘制”顺序。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-b0417.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-3280f.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
- 动画:在每个帧之间运行这些操作。大多数显示器每秒刷新屏幕60次(60 fps),当你在每一帧移动屏幕时,动画对人眼来说会很平滑(视觉停留效应)。但是如果动画错过了两者之间的帧或程序运行JS时,则页面将出现卡顿。
例如用时间不确定的 setTimeout() 只会更新内存中的属性变化,由于期间隔时间和屏幕刷新时间不同步,可能导致某些帧的操作被跨跃,直接更新下一帧的图像。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-ac8d3.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-47c81.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-6e3c6.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
解决方法
- 将JS操作划分成小块,并安排在每个帧上运行 window.requestAnimationFrame(renderFunc)。兼容版本:https://github.com/darius/requestAnimationFrame
优点:
1) 省CPU时间:页面隐藏最小化时停止渲染,setTimeout持续运行。
2) 函数节流:高频率事件(resize/scroll)为防止刷新间隔内多次执行函数,只执行一次更流畅,省开销。
// 节流(throttle) + 去抖(debounce) // 把要执行的函数放在延时器中执行且只执行最后一次,且判断距上次执行的时间大于延时执行时间 function show() {} let lastTime = new Date(); let delay = 200; let timer = null; document.body.onscroll = function () { if (document.documentElement.scrollTop >= 1000) { let now = new Date(); if (now - lastTime > delay) { // 去抖 timer = setTimeout(show, 200); } else { clearTimeout(timer); // 节流 timer = setTimeout(show, 200); } lastTime = new Date(); } };
- 将非dom操作的运行任务放在webWorker中运行,避免阻塞主线程
// 任务文件 demo_workers.js var i = 0; function timedCount() { i = i + 1; postMessage(i); // 向页面发送数据 } setInterval(timedCount, 500); // 包含dom的页面 index.html let w = new window.Worker('demo_workers.js'); // 开始 w.onmessage = function (event) { document.getElementById('result').innerHTML = event.data; }; w.terminate(); // 停止
7. 合成
浏览器知道文档的结构,每个元素的样式,页面的几何形状和绘制顺序,需将信息转换为屏幕上的像素,称为光栅化。
在视口内部使用栅格部件 – chrome首次发布时处理栅格化的方式用户滚动页面,则移动光栅框架,并通过更多光栅填充缺失的部分
合成是一种将页面的各个部分分层,分别栅格化,并在合成器线程的单独线程中合成为页面的技术。如果发生滚动,图层已经被栅格化需要合成一个新帧。通过移动图层和合成新帧,可以以相同的方式实现动画。
7.1 分层
为了找出哪些元素需要在哪些层中,主线程遍历布局树以创建层树。如果页面的某些部分应该是单独的图层(如滑入式侧面菜单)但没有得到单独图层,可以使用CSS属性will-change提示浏览器。https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-change
过多的图层:过多图层合成可能会导致比页面小部分每帧光栅化更慢。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-d26ff.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
7.2 光栅和复合关闭主线程
1) 提交合成:一旦创建了层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。
2) 栅格化:合成器线程然后栅格化每个层。一个图层可能像页面的整个长度一样大,因此合成器线程将它们分成多个图块并将图块发送到栅格线程。
3) 栅格存储:栅格线程栅格化每个图块并将它们存储在GPU内存中。
4) 绘制四边形:一旦图块被光栅化,绘制四边形的图块信息(图块在内存中的位置、绘制图块页面中的位置)
5) 合成框架:合成器线程可以优先考虑视口(或附近)内的删格线程,以便优先被光栅化。图层还有不同分辨率的倾斜度,可以处理放大操作。
6) 创建合成器帧:收集绘制四边形的图块信息,通过IPC将合成器框架提交给浏览器进程
7) 浏览器UI合成:UI线程添加另一个合成器框架以用于浏览器UI更改,或者从其他渲染器进程添加扩展。
8) GPU展示:合成器帧被发送到GPU以在屏幕上显示。
9) 滚动事件:合成器线程会创建另一个合成器帧以发送到GPU
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-5f703.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
创建磁贴位图并发送到GPU的栅格线程
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-41ad2.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
合成器线程创建合成框架,将名称发送到浏览器进程然后发送到GPU
合成的好处
- 不涉及主线程的情况下完成
- 合成线程不需要等待样式计算或JavaScript执行。
因此合成动画 被认为是平滑性能的最佳选择。如果需要再次计算布局或绘图,则必须涉及主线程。
五、页面操作
1. 接收输入事件
1) 浏览器进程接收 键入、鼠标事件、触摸手势等输入事件。浏览器进程仅知道手势发生位置,选项卡内部内容由渲染进程处理。
2) 浏览器进程将事件类型、坐标发送给渲染进程
3) 渲染进程通过查找事件目标并运行附加的事件侦听器来适当地处理事件
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-35126.jpeg)
4) 合成器接入输入事件
2. 非快速可滚动区域
1) 合成页面时,合成器线程标记页面的一个区域,该区域将事件处理程序附加为“非快速可滚动区域”。
2) 通过获取此信息,合成器线程可以确保在该区域中发生事件时将输入事件发送到运行JavaScript的主线程。如果输入事件来自该区域之外,则合成器线程在不等待主线程的情况下继续合成新帧。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150226-605e3.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
3. 事件处理
下面程序中,整个页面都被标记为非快速可滚动区域,合成器线程也必须与主线程通信,并在每次输入事件进入时等待它,最终影响合成器平滑滚动能力。
document.body.addEventListener('touchstart', event = > { if (event.target === area) { event.preventDefault(); } });
解决方案
在事件监听器中传递 passive: true 选项,提示浏览器在主线程中监听事件,合成器线程也可以继续合成新帧。
document.body.addEventListener( 'touchstart', event = > { if (event.target === area) { event.preventDefault(); } }, { passive: true });
4. 检查取消事件
通过 event.cancelable和event.preventDefault()检查取消事件。也可通过CSS来完全消除事件处理程序。
document.body.addEventListener( 'pointermove', event = > { if (event.cancelable) { event.preventDefault(); } }, { passive: true });
#area { touch - action: pan - x; }
5. 查找event.target
当合成器线程向主线程发送输入事件时,首先要运行的是命中测试以查找事件目标。命中测试使用在渲染过程中生成的绘制记录数据来找出事件发生的点坐标下面的内容。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150227-8c663.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
6. 最小化事件派发到主线程
- 屏幕刷新率:60 次/秒
- 触摸屏触摸事件:60-120次/秒
- 鼠标:100次/秒
输入事件具有比屏幕刷新更高的保真度。主线程中触发过快的连续事件,会触发过多的命中测试和JS执行,导致页面抖动。为减少对主线程过度调用,Chrome合并连续事件(如 wheel,mousewheel,mousemove,pointermove, touchmove)并延迟调度,直到下一个requestAnimationFrame执行。
离散事件则立即执行,如keydown,keyup,mouseup,mousedown,touchstart,touchend
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150227-6eeda.jpeg)
(图片引自Mariko Kosaka的《Inside look at modern web browser》)
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150227-4400e.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
7. 使用getCoalescedEvents得到帧内事件
大多数Web应用程序,合并事件应足以提供良好的用户体验。构建绘制应用程序并根据touchmove坐标放置路径等可能会丢失中间坐标以绘制平滑线,可以使用getCoalescedEvents指针事件中的方法来获取这些合并事件的信息。
![用了十几年的浏览器,你却只会浏览](http://www.liulanqi.com/d/uploads/2019/08/20190817150227-b84dd.jpeg)
图片引自Mariko Kosaka的《Inside look at modern web browser》
window.addEventListener('pointermove', event = > { const events = event.getCoalescedEvents(); for (let event of events) { const x = event.pageX; const y = event.pageY; // draw a line using x and y coordinates. } });
六、小结
浏览器是一个复杂的系统,这里介绍的只是冰山一角,chromium项目也在不停地迭代更新,所以可能一段时间后,某些功能已经发生了变化。更加细节及最新的可以关注一下最新的chromium源码。
参考文献
1、Inside look at modern web browser:https://developers.google.com/web/updates/2018/09/inside-browser-part1
2、V8:https://v8.dev
3、JavaScript module:https://v8.dev/features/modules
4、Gpu-accelerated-compositing:https://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome
5、Chrominum-design-documents:https://www.chromium.org/developers/design-documents
6、How Blink works:https://docs.google.com/document/d/1aitSOucL0VHZa9Z2vbRJSyAIsAz24kX8LFByQ5xQnUg/edit
7、Life of a Pixel 2018:https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit
作者简介:
龙付成,15年加入腾讯,资深高级前端工程师。负责过QQ浏览器游戏平台、天宫活动系统、搜索页面生成引擎、领域组件库等项目。爱好技术研究、总结和分享,曾在腾讯课堂直播《Web前端安全与实践》课程。
文章TAG:十几 浏览 浏览器 用了十几年的浏览器加载全部内容