Skip to content

浏览器渲染过程

发布于  at 10:54

多进程架构

最新的 Chrome 浏览器架构,它采用的是多进程架构。

进程分工

进程负责
browser浏览器基本功能,包括导航栏、导航按钮、书签、网络请求、文件读写等。
Chrome 在不同性能的硬件上有不同的表现
renderer * ntab 内和网页展示相关的所有工作,主要是将 HTML,CSS,以及 JavaScript 转变为我们可以进程交互的网页内容。
Chrome 会尽可能为每一个 tab 甚至是页面里面的每一个 iframe 都分配一个单独的进程
plugin * n网页所使用的扩展程序、插件
GPU处理来自不同 tab 的渲染请求并把它在同一个界面上画出来

好处

有很好的容错性:当其中一个 tab 的崩溃时,你可以随时关闭这个 tab 并且其他 tab 不受到影响。

可以提供安全性和沙盒性(sanboxing)

缺点

内存消耗大。不过进程数达到一定数量后,Chrome 会将访问同一个网站的 tab 都放在一个进程里面跑。

Browser 进程的线程

线程工作
UI thread括绘制浏览器顶部按钮和导航栏输入框等组件
network thread管理网络请求
storage thread控制文件读写

Renderer 进程的线程

GUI 渲染线程:

  1. 负责渲染浏览器界面,解析 html,css,构建 dom 树和 render 树,布局和绘制。
  2. 当重绘和回流的时候就会执行这个线程
  3. GUI 渲染线程和 js 引擎线程互斥,当 js 引擎执行时,GUI 线程就会被挂起(相当于冻结了),GUI 更新会被保存在一个队列中等到 js 引擎空闲时立即执行。

JavaScript 引擎线程:

  1. 也称 js 内核,负责处理 js 脚本程序,例如 v8 引擎
  2. 负责解析 js 脚本,运行代码
  3. 等待任务队列中的任务,一个 tab 页只有一个 js 进程
  4. 因为与 GUI 渲染线程互斥,所以 js 执行过长时间,就会造成页面渲染不连贯,导致页面渲染阻塞

为什么互斥?

避免多线程下的竞态条件(race conditions)或者数据同步问题:如果 JavaScript 引擎线程和渲染线程同时操作 DOM,可能会导致数据不一致或者页面渲染错误。

事件触发线程:

  1. 归属于浏览器而不是 js 引擎,用了控制事件循环
  2. 当 js 引擎执行 settimeout 类似的代码块时,会将对应任务添加到事件线程
  3. 当对应的事件符合触发条件时,会被放到任务队列的队尾,等待 js 引擎线程处理
  4. 由于 js 单线程的关系,这些等待处理的事件都需要排队等待 js 引擎处理

定时器触发线程:

  1. settimeout 和 setinterval 所在的线程
  2. 浏览器定时计数器不是由 js 引擎线程计数的,因此通过单独线程来计时触发定时,计时完毕后,添加到事件队列,等待 js 引擎执行。

异步 HTTP 请求进程:

  1. 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求。
  2. 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行

worker thread

web worker 和 service worker

raster thread 栅格线程

将绘制指令转化为位图的过程(将文档结构,元素的样式,元素的几何信息以及它们的绘画顺序转化为显示器的像素的过程)叫做栅格化。只栅格化视口内的内容,滚动时追加内容,类似懒加载。GPU 可以参与加速。

compositor thread 合成线程

将页面分成若干层,然后分别对它们进行栅格化。

为什么

试想一下如果有 123 三层,其中 1,2 两层没变化,第 3 层旋转了,那么只要对第三层每帧进行变换就可以得到每一帧的输出,计算量大大减少。

分层是为了减少计算、加速渲染效率。

在合成线程执行是为了不影响渲染主线程的任务执行

好处

Layer 没有父子关系,是一个平级的列表,但是还是保留 LayerTree 的名称

复合图层的形成条件

合成层中的“层”可以被认为是真正物理上的层,浏览器把它独立出来,单独拿给 GPU 处理,而层叠上下文的“层”则是指渲染层,更像是一个概念上的层,一个合成层可以包含多个渲染层;

当页面的层超过一定的数量后,层的合成操作要比在每个帧中栅格化页面的一小部分还要慢,因此衡量你应用的渲染性能是十分重要的一件事情。

输入 URL 全过程

处理输入

首先是浏览器进程的 UI 线程处理你的输入,先判断是否是合法 URL,如果不是,按照搜索关键词处理,就是拼接成一个搜索 URL。

开始导航

UI 线程会叫网络线程(network thread)初始化一个网络请求来获取站点的内容。这时候 tab 上会展示一个提示资源正在加载中的旋转圈圈,而且网络线程会进行一系列诸如 DNS 寻址以及建立 TLS 连接的操作。

如果网络线程收到服务器的 HTTP 30x 重定向响应,它就会告知 UI 线程进行重定向然后它会再次发起一个新的网络请求。

这里有一个细节:在导航开始的时候,网络线程会根据请求的域名在已经注册的 service worker 作用范围里面寻找有没有对应的 service worker。如果有命中该 URL 的 service worker,UI 线程就会为这个 service worker 启动一个渲染进程(renderer process)来执行它的代码。Service worker 既可能使用之前缓存的数据也可能发起新的网络请求。

如果启动的 service worker 最后还是决定发送网络请求的话,浏览器进程和渲染进程这一来一回的通信包括 service worker 启动的时间其实增加了页面导航的时延。这个可以在 service worker 启动的时候并行加载对应资源的方式来加快整个导航过程,这种技术叫做导航预加载。预加载资源的请求头会有一些特殊的标志来让服务器决定是发送全新的内容给客户端还是只发送更新了的数据给客户端。

读取响应

网络线程在收到 HTTP 响应的主体(payload)流(stream)时,在必要的情况下它会先检查一下流的前几个字节以确定响应主体的具体媒体类型(MIME Type)。这里仅讨论 content-type: text/html 类型的响应。

网络线程在把内容交给渲染进程之前还会对内容做安全检查。

准备一个一个渲染进程

在网络线程做完所有的检查后并且能够确定浏览器应该导航到该请求的站点,它就会告诉 UI 线程所有的数据都已经被准备好了。UI 线程在收到网络线程的确认后会为这个网站准备一个渲染进程(renderer process)来渲染界面。

为了缩短导航时间,渲染进程在网络进程开始干活的时候就提前开始准备了。

如果一切顺利的话(没有重定向之类的东西出现),网络线程准备好数据后页面的渲染进程已经就准备好了,这就节省了新建渲染进程的时间。不过如果发生诸如网站被重定向到不同站点的情况,刚刚那个渲染进程就不能被使用了,它会被摒弃,一个新的渲染进程会被启动。

导航

浏览器进程(browser process)会通过 IPC 告诉对应渲染进程去提交本次导航(commit navigation)并把响应流给到。

导航栏会被更新,会话历史也会记录这一次导航。

初始加载完成

当导航提交完成后,渲染进程开始着手加载资源以及渲染页面。我会在后面讲述渲染进程渲染页面的具体细节。一旦渲染进程“完成”(finished)渲染,它会通过 IPC 告知浏览器进程(注意这发生在页面上所有帧(frames)的 onload 事件都已经被触发了而且对应的处理函数已经执行完成了的时候),然后 UI 线程就会停止导航栏上旋转的圈圈。

我这里用到“完成”这个词,因为后面客户端的 JavaScript 还是可以继续加载资源和改变视图内容的。

导航到不同的站点

一个最简单的导航情景已经描述完了!可是如果这时用户在导航栏上输入一个不一样的 URL 的时候,浏览器在重新导航之前,会询问当前渲染进程需不需要处理一下 beforeunload 事件。

如果是点击了链接或按钮导致的重新导航,渲染进程会自己先检查一个它有没有注册 beforeunload 事件的监听函数,如果有的话就执行,执行完后发生的事情就和之前的情况没什么区别了,唯一的不同就是这次的导航请求是由渲染进程给浏览器进程发起的。

Service Worker 场景

Service worker 可以用来写网站的网络代理(network proxy),所以开发者可以对网络请求有更多的控制权,例如决定哪些数据缓存在本地以及哪些数据需要从网络上面重新获取等等。

service worker 在注册的时候,它的作用范围(scope)会被记录下来。在导航开始的时候,网络线程会根据请求的域名在已经注册的 service worker 作用范围里面寻找有没有对应的 service worker。如果有命中该 URL 的 service worker,UI 线程就会为这个 service worker 启动一个渲染进程(renderer process)来执行它的代码。Service worker 既可能使用之前缓存的数据也可能发起新的网络请求。

如果启动的 service worker 最后还是决定发送网络请求的话,浏览器进程和渲染进程这一来一回的通信包括 service worker 启动的时间其实增加了页面导航的时延。这个可以在 service worker 启动的时候并行加载对应资源的方式来加快整个导航过程,这种技术叫做导航预加载。预加载资源的请求头会有一些特殊的标志来让服务器决定是发送全新的内容给客户端还是只发送更新了的数据给客户端。

渲染进程里面发生的事

渲染进程负责标签(tab)内发生的所有事情。在渲染进程里面,主线程(main thread)处理了绝大多数你发送给用户的代码。如果使用了 web worker 或者 service worker,相关的代码将会由工作线程(worker thread)处理。合成(compositor)以及栅格(raster)线程运行在渲染进程里面用来高效流畅地渲染出页面内容。

渲染进程的主要任务是将 HTML,CSS,以及 JavaScript 转变为我们可以进行交互的网页内容。

解析

渲染进程在导航结束的时候会收到来自浏览器进程提交导航(commit navigation)的消息,在这之后渲染进程就会开始接收 HTML 数据,同时主线程也会开始解析接收到的文本数据(text string)并把它转化为一个 DOM(Document Object Model)树。其实就一个编译过程,包括令牌化、词法解析、语法解析。

网站通常还会使用到一些诸如图片,CSS 样式以及 JavaScript 脚本等资源,主线程会按照在构建 DOM 树时遇到各个资源的循序一个接着一个地发起网络请求。

可以为 script 标签添加一个 async 或者 defer 属性甚至使用 ES Module 来使 JavaScript 脚本进行异步加载。同时 <link rel="preload"> 资源预加载可以用来告诉浏览器这个资源在当前的导航肯定会被用到,你想要尽快加载这个资源。

布局

与此同时,主线程会解析 CSS 的响应数据,并计算每一个 DOM 节点的样式,并构建出一颗布局树(layout tree)。 布局树上每个节点会有它在页面上的 x,y 坐标以及盒子大小。

布局树长得和先前构建的 DOM 树差不多,不同的是这颗树只有那些可见的(visible)节点信息。而且伪类不会出现在 DOM 树上,会出现在布局树上。

布局的过程极其复杂,一句话总结就是:从上而下,从左到右一个一个的排列,排不下换行。

绘制

主线程会遍历之前得到的布局树来生成层叠上下文。其实就是对图层的三维构想,通过绘制的先后顺序来达到覆盖与被覆盖的关系。

如果元素有动画效果(animating),浏览器就不得不在每个渲染帧的间隔中通过渲染流水线来更新页面的元素。

合成 & 栅格

到目前为止,浏览器已经知道了关于页面以下的信息:文档结构,元素的样式,元素的几何信息以及它们的绘画顺序。接下来就是将以上这些信息转化为转化为位图(也就是显示器的像素),这个过程叫栅格化,是 GPU 计算的。但是在栅格化之前,浏览器还会有一个分层的过程,将页面分成若干层,然后分别对它们进行栅格化,最后在合成线程(compositor thread)里面合并成一个页面。

为了确定哪些元素需要放置在哪一层,主线程需要遍历渲染树来创建一棵层次树(Layer Tree)(在 DevTools 中这一部分工作叫做“Update Layer Tree”)。可以通过使用 will-change CSS 属性来告诉浏览器对其分层。

当页面的层超过一定的数量后,层的合成操作要比在每个帧中栅格化页面的一小部分还要慢,因此衡量你应用的渲染性能是十分重要的一件事情。

一旦页面的层次树创建出来并且页面元素的绘制顺序确定后,主线程就会向合成线程(compositor thread)提交这些信息。然后合成线程就会栅格化页面的每一层,每一层又会被切割成更小的图块发送给一系列栅格线程(raster threads)。

上面的步骤完成之后,合成线程就会通过 IPC 向浏览器进程(browser process)提交(commit)一个渲染帧。这些合成帧都会被发送给 GPU 从而展示在屏幕上。如果合成线程收到页面滚动的事件,合成线程会构建另外一个合成帧发送给 GPU 来更新页面。

合成的好处在于这个过程没有涉及到主线程,所以合成线程不需要等待样式的计算以及 JavaScript 完成执行。这也就是为什么说只通过合成来构建页面动画是构建流畅用户体验的最佳实践的原因了。如果页面需要被重新布局或者绘制的话,主线程一定会参与进来的。

关于滚动事件的优化

当页面不存在任何用户事件的监听器(event listener),合成线程完全不需要主线程的参与就能创建一个新的合成帧来响应事件。

当页面有一些事件监听器(event listeners),合成线程会将页面那些注册了事件监听器的区域标记为“非快速滚动区域”(Non-fast Scrollable Region)。当用户事件发生在这些区域时,合成线程会将输入事件发送给主线程来处理。如果输入事件不是发生在非快速滚动区域,合成线程就无须主线程的参与来合成一个新的帧。

所以在 body 上添加事件处理程序很容易就给合成线程造成额外负担,可以为事件监听器传递 passive:true 选项。 这个选项会告诉浏览器虽然主线程在侦听事件,但是合成线程也可以继续合成新的帧,不需要等主线程。

  1. 硬件加速并不是前端专有的东西,它是一个很宽泛的计算机概念——把软件的工作交给特定的硬件,更高效的完成某项任务。对于前端来说,就是使用特定的 CSS 属性,把元素提升成合成层,交给 GPU 处理;
  2. 合成层中的“层”可以被认为是真正物理上的层,浏览器把它独立出来,单独拿给 GPU 处理,而层叠上下文的“层”则是指渲染层,更像是一个概念上的层,主要用来记录绘制顺序,一个合成层可以包含多个渲染层;
  3. 层爆炸指的是大量元素意料之外被提升成合成层,即隐式合成;层压缩是浏览器对隐式合成的优化,chrome 在 94 版本中做到比较完善了;
  4. 使用 transform、opacity 取代传统属性来实现一些动画,并把他们提升到一个单独的合成层,能跳过布局计算和重新绘制,直接合成,能避免不必要的回流、重绘;

https://mp.weixin.qq.com/s/-VIpnfzHZy5fYwI9PECayQ

https://zhuanlan.zhihu.com/p/102149546

https://blog.poetries.top/browser-working-principle/guide/part1/lesson04.html#_1-%E7%94%A8%E6%88%B7%E8%BE%93%E5%85%A5

分享到: