前言
之前也看过浏览器工作原理,不过那时候知识储备还不够,看起来云里雾里,看完也没有总结,一大把的功夫就打水漂了。
今天心血来潮,突然想看看浏览器工作原理,找鸭找,终于找到了我最爱的那种文章。
正是Inside look at modern web browser系列。看完之后,觉得意犹未尽,正好想总结一下,于是就打算写这一篇记录。(通文中其他链接一样,,,进入该链接需要“机缘”)
(在看完三篇之后,发现这系列文章在之前的掘金翻译计划中有参与,在遗憾没有第一时间发现的同时,想到能更方便自己平时的浏览,也就释然了。在这里先分享出来,不过我还没怎么看过译版,大家自己评价。)
第一篇
说明:由于不缺翻译,我打算仅记录个人认为关键的点。
As part 1 of this series, we’ll take a look at core computing terminology and Chrome’s multi-process architecture.
本系列博客的第一篇,讲的是基本术语与Chrome的多进程架构。
If two processes need to talk, they can do so by using Inter Process Communication (IPC).
进程之间通过IPC
技术通讯。(文内不特意强调)
Chrome的多进程架构:
Process and What it controls Browser Controls "chrome" part of the application including address bar, bookmarks, back and forward buttons. Also handles the invisible, privileged parts of a web browser such as network requests and file access. Renderer Controls anything inside of the tab where a website is displayed. Plugin Controls any plugins used by the website, for example, flash. GPU Handles GPU tasks in isolation from other processes. It is separated into different process because GPUs handles requests from multiple apps and draw them in the same surface.
主要用到的是:浏览器进程、渲染进程、插件进程、GPU进程。
- 浏览器进程:控制应用中的 “Chrome” 部分,包括地址栏,书签,回退与前进按钮。以及处理 web 浏览器不可见的特权部分,如网络请求与文件访问。
- 渲染进程:控制标签页内网站展示。
- 插件进程: 控制站点使用的任意插件,如 Flash。
- GPU进程:处理独立于其它进程的 GPU 任务。GPU 被分成不同进程,因为 GPU 处理来自多个不同应用的请求并绘制在相同表面。
还有些进程,但不那么关心,如扩展(Extension )进程、工具(utility)进程。
简单总结一下浏览器多进程架构的优点:
- 通过开启多个渲染进程,使得标签页之间相对独立,一般不会因一个标签页崩溃而整个程序崩溃。(不一般是指内存不够时,相同站点的标签页会由同一个渲染进程负责,当然,此时开启进程的数量也会有限制。)
- 安全性与封闭性(sandbox)好。浏览器通过操作系统提供的方法,限制某些进程的权限。例如,限制渲染进程对文件的访问权限。
浏览器的架构正在更新:
下一个方向是通过将模块服务化,自适应设备的资源,聚合成一个或分解成多个进程。(待考究是否已经更改)
Chrome针对安全攻击的优化:
即不同站点的页面由不同的渲染进程展示,即使他们在同一个tab(如利用iframe
)。
第二篇
In the previous post, we looked at how different processes and threads handle different parts of a browser. In this post, we dig deeper into how each process and thread communicate in order to display a website.
第二篇,讲浏览器是怎样通过协调进程与线程,来展示页面的。
接下来的整体流程是真的清晰,因为它是从浏览器的角度来说的。
What happens in navigation——从输入url到呈现页面
第一步:处理输入
When a user starts to type into the address bar, the first thing UI thread asks is "Is this a search query or URL?".
这里的UI线程是浏览器进程的一部分,负责处理标签页外的UI。
用户的输入先由它这样处理,因为Chrome的地址栏同样是搜索栏。
第二步:开始导航
When a user hits enter, the UI thread initiates a network call to get site content.
用户确认输入以后,UI线程通知浏览器进程中的网络线程(现在好像网络线程从浏览器进程中独立出来了,不过本记录仍保留原文的描述)以获取网络资源。此时UI线程将标签页左侧设置成旋转的Loading。
而网络线程,就采用各种协议请求资源。(浏览器缓存的内容与方式决定了网络线程发送请求的与否)
此时,若网络线程收到服务端发过来301的重定向状态码,则会通知UI线程更改地址栏的同时(仅更改地址),自己开始重新请求。
第三步:处理响应
当服务器返回响应时,为了确定数据类型,浏览器进程的网络线程可能会阅读响应体中的字节流。即响应头的Content-Type字段丢失或者错误时,网络线程就会进行MIME Type sniffing。
The definition of how browsers should interpret media types and figure out what to do with content that doesn't have a valid one
当确定了响应的类型,例如HTML文件(输了URL肯定先要HTML),浏览器进程的网络线程会把数据送给渲染进程。
(脱离我们的导航场景,当响应数据为zip等其他非渲染页面要用的文件时,网络线程会将数据传给下载管理器(download manager
))
在网络线程传数据给其他模块之前,会进行一些安全检测,如safeBrowsing和CORB。
第四步:准备渲染进程
当检查完毕,并且网络线程确定浏览器应该导航至请求的网站时,网络线程通知UI线程数据已经准备完毕,经由UI线程的手,准备渲染进程。
由于网络线程是需要等待响应的(可能会数百毫秒),此处应用了一个优化。
UI线程实际上会在网络线程发出请求的同时寻找或开启一个渲染进程,不出意外,当网络线程接收到响应式,渲染进程应该已经准备好了。(但当响应是跨站重定向时,将不会使用准备好的进程,而是重新开启新的进程,整个过程也会慢一些。)
第五步:提交导航
当渲染进程和数据都准备好,浏览器进程就会向渲染进程提交本次导航,附带响应首部(当然还有数据,由网络线程和渲染进程直接建立数据通道的方式传输)。一旦浏览器进程接收到渲染进程对此次提交的确认,导航就已经完成了,而页面加载阶段开始。
此时,浏览器的地址栏会进行更新,如显示安全标志和网站信息等等。浏览历史(保存在磁盘上)也会更新以保证前进后退的使用(此时Loading并未停止)。
第六步:页面加载
(以上是导航的过程,接下来就是渲染进程负责的页面加载了。但页面加载也挺复杂,所以作者将页面加载放在了第三篇。)
额外的步骤:初始化加载完成
跳过页面加载,渲染进程在加载“完成”之后,会通知浏览器进程(所有onLoad
事件已经完成时),此时浏览器进程的UI线程才会停止旋转Loading。
上面的“完成”打引号是因为JS可能会加载额外的资源并导致渲染新的视图。
重新导航
以上是第一次输入URL的导航过程。重新导航至其他网站,会在之前的基础上多出一个处理beforeunload
事件的过程(JS代码,当然是运行在渲染进程)。另外,如其名,这个过程是在导航新页面之前的。
(导航至其他网站有两种方式,一是输入URL,二是JS和链接。两种方式导致的区别只在于浏览器进程与渲染进程中,谁是主动通信的一方。)
beforeunload
事件处理之后,就会启动新的渲染进程,旧的进程则处理像unload
这样的事件。更多详细信息可参阅 an overview of page lifecycle states 和 the Page Lifecycle API.
如果有Service Worker
虽然我对Service Worker还不了解,但不耽误我理解浏览器对它的处理。
当浏览器发现Service Worker的存在时,并且新的导航被Service Worker所关联(或者说注册),那么将会有一个渲染进程来执行Service Worker的代码,并由其决定究竟是使用缓存还是发送网络请求。
(由于等待Service Worker确认是否发起网络请求,所以会有延迟,Chrome对此也做了优化,即Service Worker开始的同时,标记并发起一些请求,这些请求可由服务器来决定响应的具体内容。)
It marks these requests with a header, allowing servers to decide to send different content for these requests; for example, just updated data instead of a full document.
第三篇
第三篇主要介绍了第二篇中的第六步。
The renderer process's core job is to turn HTML, CSS, and JavaScript into a web page that the user can interact with.
渲染进程的主要职责,就是将HTML,CSS,JS转化成一个用户可以交互的网页。
渲染进程内包含的线程
- 主线程:负责处理发送到客户端(User)的主要代码
- 合成线程:处理合成任务
- 栅格化线程:处理栅格化任务
- 工作线程:如果启用web worker或service worker,部分js将由工作线程处理
页面加载,启动
解析
Parsing an HTML document into a DOM is defined by the HTML Standard.
根据HTML 标准将HTML解析成DOM。
子资源加载
网站中使用到的额外资源,如图片,CSS,JS等。HTML解析器在遇到这些资源时,会生成令牌(tokens),并在构建DOM的过程中由预加载扫描器(preload scanner)处理,最终通知(send requests to the network thread)浏览器进程中的网络线程以发送请求。
HTML中的Script
HTML解析是会被阻塞的。被script
标签甚至是CSS样式。
(当Script标签内部访问了CSS,而该CSS是外部导入,需要下载的,那么此时就需要等待CSS下载完成后再执行JS代码,所以可以说是被CSS阻塞)
样式计算
在主线程(别忘了几个线程的名字)解析完DOM后(不被阻塞),会开始计算样式。
布局
同样,由主线程根据DOM和样式来生成布局树。它是一个类似于DOM,保存着所有位置关系的树形结构。(所有位置信息,所以overflow: hidden
的节点和伪元素都会包含在内,而display: none
的节点,因为它没有位置信息,所以不在内。)
绘制
为了绘制界面,还需要计算一个绘制顺序——绘制记录。就像:
Paint record is a note of painting process like "background first, then text, then rectangle".
合成
按理来说,知道以上所有的信息(DOM、样式、布局树、绘制记录)之后,就可以生成页面了,U1S1,确实,Chrome刚发布的时候确实是这样的,但后面做了优化,那就是合成。
Compositing is a technique to separate parts of a page into layers, rasterize them separately, and composite as a page in a separate thread called compositor thread.
合成呢,是将网页分成很多层(方便处理层叠和3D),让他们分开栅格化(不合成的话,生成页面栅格化就完事儿了),最后在合成线程中合成他们。
(层叫起来拗口死了,后面叫他图层)
说起来简单,但合成呢,也不是一次性把所有东西都合成的,毕竟那样分开还有什么意义。
这里举个栗子。当滚动发生的时候,之前的图层已经栅格化了,所以只要合成一个新的图像(frame,帧,画面->图像)就行了。
???什么乱七八糟的
奥,这里的图像是并不是我们想象中的一个很大的图像,概念后面再说,反正它并不一定是一个屏幕那么大。所以说滚动了只要合成一个图像就完事儿了。(动画也是这么搞)
那么网页分成很多图层?怎么个分法?
奥,这还是主线程干的活。
通过布局树来生成图层树。
那不是没我们开发者什么事儿?那到不是,还是能干点事儿的。
If certain parts of a page that should be separate layer (like slide-in side menu) is not getting one, you can hint to the browser by using
will-change
attribute in CSS.
如果你觉得他可以多分点,那你就通过will-change
属性叫他分。
(物极必反,图层分多了,效率还不如不合成呢,想了解的戳 Stick to Compositor-Only Properties and Manage Layer Count)
合成线程干活啦
图层树生成完了,绘制记录也有了,那就该栅格化线程出场了。主线程将这俩信息给合成线程,合成线程就把图层全部都给栅格化线程。
???合成线程干了个啥,打酱油?
原来图层太大啦,由合成线程操刀,把他都分成小片(divides them into tiles)。
然鹅,一个图层分出的小片却可以组合成不止一个图层,因为有:
multiple tilings for different resolutions to handle things like zoom-in action
然后栅格化线程(与GPU进程通信,多个栅格化线程组成栅格化线程池,其意义就是防止GPU进程的栅格化任务阻塞),GPU进程将这些小片栅格化,就存进了GPU内存里。其中,在视口内或附近的小片会被优先栅格化。
当小片被栅格化之后,合成线程就又跳出来了。
compositor thread gathers tile information called draw quads to create a compositor frame.
他会收集记录小片信息的draw quads,然后生成合成图像。
Draw quads Contains information such as the tile's location in memory and where in the page to draw the tile taking in consideration of the page compositing. Compositor frame A collection of draw quads that represents a frame of a page.
合成图像生成之后,就被提交给浏览器进程,同时,合成线程又能生成其他的图像(为页面或为浏览器UI)。浏览器进程又把合成图像扔给GPU,让它显示(可能是权限问题,所以要经由浏览器进程之手)。
像刚刚说的,出现滚动事件,合成线程合成新的图像就完事儿了。
(合成说起来感觉很复杂,可是这都不是主线程干的事儿,所以不需要像主线程那样会被HTML、CSS、JS耽搁,除非要重新生成图层树。)
更新渲染是costly的
我们常说回流和重绘,就是由于浏览器高花销的渲染流程。(这里的高花销并不是指其占用的资源很多,而是指在渲染的过程中,主线程的每个结果都是根据之前得出的结果生成的。)
第四篇(待续,还是想看看原文)
感谢观看
该文章首发于个人博客,若文中存在理解错误的地方,欢迎大佬指正。
author: Jankin
authorLink: https://www.laic.club
comments: true
tags:
- Web Broswer
- Record
date: 2020-07-16 20:32:59