作为前端开发,我们的日常工作中除了编码以外,几乎大多数时间都在跟浏览器打交道。所以我们更加要吃透浏览器,掌握它到底是怎样将我们编写的代码渲染到页面中的。
所以,今天我主要结合浏览器的内部工作原理,深入剖析下浏览器中页面的渲染过程。
第 6 讲我们介绍了一个 HTTP 请求在浏览器中的请求过程,该过程将浏览器作为单独的对象,描述客户端和服务端之间的通信过程。那么,当我们在浏览器的地址栏中输入 URL,按下回车键,到页面在浏览器中渲染完成,这个过程中浏览器的内部发生了什么了呢?
为了了解这个过程,首先我们要了解浏览器的内部结构。
从结构上来说,浏览器主要包括了八个子系统:用户界面、浏览器引擎、渲染引擎、网络子系统、JavaScript 解释器、XML 解析器、显示后端、数据持久性子系统。
这些子系统组合构成了我们的浏览器。页面的加载和渲染过程,离不开网络子系统、渲染引擎、JavaScript 解释器和浏览器引擎。
以前端开发最常使用的 Chrome 浏览器为例, Chrome 浏览器是使用多进程架构的方式来管理这些子系统。
Chrome 浏览器采用的多进程架构,主要包括四个进程:
浏览器进程:选项卡之外的所有内容都由浏览器进程处理,浏览器进程则主要用于控制和处理用户可见的 UI 部分(包括地址栏,书签,后退和前进按钮)和用户不可见的隐藏部分(例如网络请求和文件访问)。
GPU 进程:该进程用于完成图像处理任务,同时还支持分解成多个进程进行处理。
渲染器进程:Chrome 浏览器中支持多个选项卡,其中每个选项卡在单独的渲染器进程中运行,渲染器进程主要用于控制和处理选项卡中的网站内容显示。
插件进程:管理 Chrome 浏览器中的各个插件。
对于“在浏览器的地址栏中输入 URL,按下回车键,到浏览器渲染页面”这个过程,浏览器内部会通过浏览器进程和渲染器进程,进行很多交互逻辑,最终才得以将页面内容显示在屏幕上。
其中,浏览器进程和渲染器进程同样支持多线程,包括以下这些线程。
这些线程其实并不陌生,在前面介绍的内容中有提到,比如:
在页面的加载过程中,涉及 GUI 渲染线程与 JavaScript 引擎线程间的互斥关系,因此页面中的和
元素设计不合理会影响页面加载速度;
在 UI 线程、网络线程、存储线程、浏览器事件触发线程、浏览器定时器触发线程中,I/O 事件通过异步任务完成时触发的函数回调,解决了单线程的 Javascript 阻塞问题。
下面我们再来看下 Chrome 浏览器中页面的渲染过程,包括浏览器进程和线程如何通信来显示页面。
首先我们将浏览器中页面的渲染过程分为两部分。
页面导航:用户输入 URL,浏览器进程进行请求和准备处理。
页面渲染:获取到相关资源后,渲染器进程负责选项卡内部的渲染处理。
当用户在地址栏中输入内容时,浏览器内部会进行以下处理。
首先浏览器进程的 UI 线程会进行处理:如果是 URI,则会发起网络请求来获取网站内容;如果不是,则进入搜索引擎。
如果需要发起网络请求,请求过程由网络线程来完成。HTTP 请求响应如果是 HTML 文件,则将数据传递到渲染器进程;如果是其他文件则意味着这是下载请求,此时会将数据传递到下载管理器。
如果请求响应为 HTML 内容,此时浏览器应导航到请求站点,网络线程便通知 UI 线程数据准备就绪。
接下来,UI 线程会寻找一个渲染器进程来进行网页渲染。当数据和渲染器进程都准备好后,HTML 数据通过 IPC 从浏览器进程传递到渲染器进程中。
渲染器进程接收 HTML 数据后,将开始加载资源并渲染页面。
渲染器进程完成渲染后,通过 IPC 通知浏览器进程页面已加载。
以上是用户在地址栏输入网站地址,到页面开始渲染的整体过程。为了方便理解,我帮你梳理了一个流程图:
如果当前页面跳转到其他网站,浏览器将调用一个单独的渲染进程来处理新导航,同时保留当前渲染进程来处理像unload
这类事件。
在上面的过程中可以看到,页面导航主要依赖浏览器进程。其中,上述过程中的步骤 5 便是页面的渲染部分,该过程同样依赖渲染器进程,我们一起来看看。
前面说过,渲染器进程负责选项卡内部发生的所有事情,它的核心工作是将 HTML、CSS 和 JavaScript 转换为可交互的页面。
整体上,渲染器进程渲染页面的流程基本如下。
解析(Parser):解析 HTML/CSS/JavaScript 代码。
布局(Layout):定位坐标和大小、是否换行、各种position
/overflow
/z-index
属性等计算。
绘制(Paint):判断元素渲染层级顺序。
光栅化(Raster):将计算后的信息转换为屏幕上的像素。
大致流程如下图:
我们来分别看下。
1. 解析。
渲染器进程的主线程会解析以下内容:
解析 HTML 内容,产生一个 DOM 节点树;
解析 CSS,产生 CSS 规则树;
解析 Javascript 脚本,由于 Javascript 脚本可以通过 DOM API 和 CSSOM API 来操作 DOM 节点树和 CSS 规则树,因此该过程中会等待 JavaScript 运行完成才继续解析 HTML。
解析完成后,我们得到了 DOM 节点树和 CSS 规则树,布局过程便是通过 DOM 节点树和 CSS 规则树来构造渲染树(Render Tree)。
2. 布局。
通过解析之后,渲染器进程知道每个节点的结构和样式,但如果需要渲染页面,浏览器还需要进行布局,布局过程便是我们常说的渲染树的创建过程。
在这个过程中,像header
或display:none
的元素,它们会存在 DOM 节点树中,但不会被添加到渲染树里。
布局完成后,将会进入绘制环节。
3. 绘制
在绘制步骤中,渲染器主线程会遍历渲染树来创建绘制记录。
需要注意的是,如果渲染树发生了改变,则渲染器会触发重绘(Repaint)和重排(Reflow)。
重绘:屏幕的一部分要重画,比如某个 CSS 的背景色变了,但是元素的几何尺寸没有变。
重排:元素的几何尺寸变了(渲染树的一部分或全部发生了变化),需要重新验证并计算渲染树。
为了不对每个小的变化都进行完整的布局计算,渲染器会将更改的元素和它的子元素进行脏位标记,表示该元素需要重新布局。其中,全局样式更改会触发全局布局,部分样式或元素更改会触发增量布局,增量布局是异步完成的,全局布局则会同步触发。
重排需要涉及变更的所有的结点几何尺寸和位置,成本比重绘的成本高得多的多。所以我们要注意以避免频繁地进行增加、删除、修改 DOM 结点、移动 DOM 的位置、Resize 窗口、滚动等操作,因为这些操作可能会导致性能降低。
4. 光栅化
通过解析、布局和绘制过程,浏览器获得了文档的结构、每个元素的样式、绘制顺序等信息。将这些信息转换为屏幕上的像素,这个过程被称为光栅化。
光栅化可以被 GPU 加速,光栅化后的位图会被存储在 GPU 内存中。根据前面介绍的渲染流程,当页面布局变更了会触发重排和重绘,还需要重新进行光栅化。此时如果页面中有动画,则主线程中过多的计算任务很可能会影响动画的性能。
因此,现代的浏览器通常使用合成的方式,将页面的各个部分分成若干层,分别对其进行栅格化(将它们分割成了不同的瓦片),并通过合成器线程进行页面的合成。
合成过程如下:
当主线程创建了合成层并确定了绘制顺序,便将这些信息提交给合成线程;
合成器线程将每个图层栅格化,然后将每个图块发送给光栅线程;
光栅线程栅格化每个瓦片,并将它们存储在 GPU 内存中;
合成器线程通过 IPC 提交给浏览器进程,这些合成器帧被发送到 GPU 进程处理,并显示在屏幕上。
合成的真正目的是,在移动合成层的时候不用重新光栅化。因为有了合成器线程,页面才可以独立于主线程进行流畅的滚动。
到这里,页面才真正渲染到屏幕上。
我们在绘制页面的时候,也可能会遇到很多奇怪的渲染问题,比如使用了transform:scale
可能会导致某些浏览器中渲染模糊,究其原因则是由于光栅化过程导致的。像前面所说,前端开发需要频繁跟浏览器打交道,所谓知己知彼百战不殆,我们应该对其运行过程有更好的了解。
今天我主要介绍了浏览器的组成,可分为用户界面、浏览器引擎、渲染引擎、网络子系统、JavaScript 解释器、XML 解析器、显示后端、数据持久性子系统八个子系统,并以 Chrome 浏览器为例,从浏览器内部分工角度来介绍页面的渲染过程。
掌握页面的渲染过程,有利于我们进行一些性能优化,尤其如果涉及动画、游戏等频繁绘制的场景,渲染性能往往是需要不断进行优化的瓶颈。
今日小作业:
你认为 Chrome 浏览器中,为什么每个选项卡都在单独的渲染器进程中运行呢?
如何检测页面是否无响应呢?
把你的想法写在留言区吧!
老师,看其它的一些关于浏览器渲染过程,都是将渲染树的构建看成是一个独立的阶段呢。看了你的渲染过程不由得让我产生了究竟是渲染树的构建是和布局阶段一起的呢,还是说独立的怀疑,望指正。😅
文中介绍渲染过程,主要分为:解析(Parser)、布局(Layout)、绘制(Paint)、光栅化(Raster) 四个过程,渲染树的构建属于布局过程,应该写得比较清楚了,请问你的疑问是?
老师,是不是分层后,浏览器就能够只渲染某一层的内容,提升效率
分层的使用场景很多是一些动画效果,比如 transform 和 opacity 的使用,它们只更改影响合成的属性,不会导致重新计算和布局,会让渲染更加流畅
老师,检测页面是否无响应的使用场景可以说下吗?
可以使用 service worker 对页面进行心跳检测,当心跳断开之后就可以认为页面崩溃/无响应,将相关信息做上报就可以监测的
现代浏览器架构网络独立成一个进程了,所以打开一个tab至少包含四个进程:浏览器主进程、渲染进程、网络进程、GPU进程。关于插件,一个插件开启一个进程。
老师,您好!我看在栅格化之后进行合成,文中提到了合成线程和光栅线程。请问这两个线程是属于哪个进程呢?
合成器线程、光栅线程都运行在渲染器进程内部,从而高效,流畅地渲染页面。
避免一个选项卡挂了,整个浏览器渲染进程挂了
网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程,现在应该有五个进程了吧
本文写作时是参照《Inside look at modern web browser》该官方文章来进行说明的,当时文章中介绍除了 GPU 进程、浏览器进程、渲染器进程、插件进程以外,还有实体进程和拓展程序进程。
如今,除了以上进程,Chrome 浏览器独立出来的进程还包括 Network、Storage、Audio 等等,如果你想查看 Chrome 中正在运行多少个进程,请点击浏览器右上角的选项菜单,选择“更多工具”,然后选择“任务管理器”,就可以查看了~
事件驱动使编写代码的流程更加清晰,但代码量大,维护相对困难,改变一处往往要牵涉好几处的更改数据驱动从应用程序中分离出视图和模型,代码量更少,今后如果要改变某些数据,只需改变对应模型层上的数据
赞!
- 每个选项卡都在单独的渲染器进程中运行是为了各个选项卡能保持独立互不干扰,防止因某一个选项卡崩溃导致所有页面都崩溃。2.检查页面无响应是不是可以利用事件循环相关,比如判断setTimeout中的回调是否被执行了,无响应的话,应该是主线程卡死,宏任务和微任务的事件都没有机会运行
1. 没错。2. 当前页面无响应,是否可以考虑使用跨页面的技术来支持呢?比如 worker?(啊我竟然说出来了)