我们在八股文中经常看到一个问题:从从地址栏输入框中输入地址后到界面的最终呈现,这个过程中发生了什么?
我们先从浏览器的主要构成讲起:
浏览器进程【Browser Process】,渲染进程【Render Process】,GPU进程【GPU Process】,还有其他进程本文暂不涉及。
一、浏览器进程【Browser Process】:
主要包含UI线程,网络线程,存储线程。主要负责用户的交互,地址栏输入,前进后退,页签的创建与销毁等
1、处理输入
当我我们在地址栏输入地址按回车的时候,这一步起作用的是该进程中的UI线程起到作用,它会去判断你输入的内容是普通的搜索内容还是一个网络地址。如果是普通内容,则会直接跳转到对应的默认搜索引擎,例如直接跳转到百度搜索的结果页面。如果输入的是网络地址则会通知网络线程去获取网页内容,并且控制tab上面的图标进入loading状态,表示正在加载资源【也可以认为loading状态定停止时表示资源全部请求完成了】。
2、开始导航
网络进程这一步发生了什么呢?比如进行DNS查找,建立TLS链接【俗称三次握手】。 此时,网络线程可能会收到 HTTP 301 之类的服务器重定向标头。在这种情况下,网络线程会与服务器正在请求重定向的 UI 线程通信。然后,将发起另一个 URL 请求。
3、读取响应
一旦响应主体开始加载,网络线程会在必要时查看流的前几个字节。响应的 Content-Type 以及 MIME Type说明它是什么类型的数据。如果响应是一个HTML文件,那么下一步就是将数据传递给渲染器进程,但如果它是一个 zip 文件或其他文件,那么这意味着它是一个下载请求,所以他们需要将数据传递给下载管理器。这也是进行安全浏览检查的地方。如果域和响应数据似乎与已知的恶意站点相匹配,则网络线程会发出警报以显示警告页面。此外,发生跨源读取 (CORB)检查是为了确保敏感的跨站点数据不会进入渲染器进程。
4、与渲染器进程【Render Process】进行通信
一旦完成所有检查并且网络线程确信浏览器应该导航到请求的站点,网络线程就会告诉 UI 线程数据已准备好。由于网络请求可能需要数百毫秒才能得到响应,因此应该采用优化手段来加速此过程。
现在数据和渲染器进程已准备就绪,从浏览器进程向渲染器进程发送 IPC 以提交导航。一旦浏览器进程监听到在渲染器进程中发生提交的确认,导航就完成了,文档加载阶段开始了。
此时,地址栏更新,安全指示器和站点设置 UI 反映了新页面的站点信息。该选项卡的会话历史记录将被更新,因此后退/前进按钮将逐步浏览刚刚导航到的站点。为了在关闭选项卡或窗口时促进选项卡/会话恢复,会话历史记录存储在磁盘上。
一旦渲染器进程"完成"渲染,它会通过 IPC 将信息发送回浏览器进程(这是在 onload所有事件并完成执行之后)。此时,UI线程停止选项卡上的加载效果。
二、渲染进程【Render Process】:
渲染器进程的核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。
1、DOM的构建
当渲染器进程接收到导航的提交消息并开始接收 HTML 数据时,主线程开始解析文本字符串 (HTML) 并将其转换为文档对象模型 ( DOM )。DOM 是浏览器对页面的内部表示,也是 Web 开发人员可以通过 JavaScript 与之交互的数据结构和 API。
2、资源加载
网站通常使用image、css 和 js 等外部资源。这些文件需要从网络或缓存中加载。主线程可以在解析构建DOM的过程中找到它们时一一请求,但为了加快速度,"预加载扫描器"是并发运行的。如果 HTML 文档中有类似的东西img,link预加载扫描器会查看 HTML 解析器生成的令牌,并向浏览器进程中的网络线程发送请求。
备注:我们通常都听过前端工程中的优化,这并不是我们工作为了优化而优化,而是在浏览器渲染原理中的某些过程,让我们需要优化来加速浏览器本身的机制执行的速度。
此处我们引申出另一个问题:资源太大了怎么办,首屏白屏时间太长怎么解决?
想必大家都也在各大文章中见过解决方案:
CDN加速,精灵图,代码压缩,减少代码量,Gzip,代码分割,按需加载,启用http2,本地缓存等。
减少代码量:抽取公共方法,提取公共组件,删除无效代码等
打包工具能解决:代码压缩,代码分割,按需加载等
构建CDN服务器:CDN加速
采用http2:解决http1只有6个并发限制,但不仅限于此
缓存策略:storage,indexDB,强缓存,协商缓存等
Gzip:开启Gzip压缩
缓存策略:强缓存,协商缓存等
但是这些在原理上是优化了浏览器渲染中的哪一步的问题?在上述的内容中大家应该明白了这些优化手段最终影响的是浏览器进程中的网络线程这一步。
思考:当带宽无限大,所有资源都是瞬间完成下载时,上述的优化手段是否还有存在的必要?
3、加载JS
当 HTML 解析器找到一个script标签时,它会暂停 HTML 文档的解析,并且必须加载、解析和执行 JavaScript 代码。为什么?因为document.write()可以通过js改变整个 DOM 结构之类的东西来改变文档的形状。这就是为什么 HTML 解析器必须等待 JavaScript 运行才能恢复对 HTML 文档的解析。当然也可以添加async或defer属性到script标记。然后浏览器异步加载和运行 JavaScript 代码,并且不会阻止解析。
4、样式计算
拥有 DOM 不足以了解页面的外观,因为我们可以在 CSS 中设置页面元素的样式。主线程解析 CSS 并确定每个 DOM 节点的计算样式【CSSOM】。不提供任何 CSS,每个 DOM 节点也有一个计算样式。H1显示得比H2大,并且为每个元素定义了边距。这是因为浏览器有一个默认样式表。
5、布局
布局是寻找元素几何形状的过程。主线程遍历 DOM 和计算样式,并创建包含 xy 坐标和边界框大小等信息的布局树。布局树可能类似于 DOM 树的结构,但它只包含与页面上可见内容相关的信息。如果该元素是display: none,则该元素不是布局树的一部分(但是,visibility: hidden在布局树中)。类似地,像p::before{content:"Hi!"} 的伪类,即使它不在 DOM 中,它也会包含在布局树中。
6、绘制
拥有 DOM、样式和布局树仍然不足以呈现页面。知道了元素的大小、形状和位置,但仍然必须判断绘制它们的顺序。例如,z-index可能为某些元素设置,在这种情况下,按照 HTML 中编写的元素顺序绘制将导致不正确的呈现。在此绘制步骤中,主线程遍历布局树以创建绘制记录。绘画记录是"先背景,后文字,后矩形"的绘画过程的记录。
既然讲到了绘制,那么必须得提起回流与重绘:如果布局树发生变化,则需要为文档的受影响部分重新生成绘制顺序
回流:文档中的一部分或全部元素因为规模尺寸、布局、隐藏等改变而需要浏览器重新计算位置和大小,这个过程称为回流。
当其中的一个元素的大小,位置,布局方式发生了改变,需要重新计算其他的元素的布局信息,并且生成新的绘制记录。这些计算也在主线程上运行,我们都知道JS是单线程的,当这个计算时间超过每一帧的动画执行时间【电脑显示屏的FPS,1/FPS】,人眼就会感觉到卡顿。所以我们常见的约定中会有尽可能减少回流的操作。

重绘:当元素的一部分属性发生改变并不影响它在文档流中的位置(即不会引起布局的变化),也可以理解成不影响其他元素的方式

三、GPU进程【GPU Process】:
合成与分层依据浏览器的不同会在Render Process和GPU Process中进行,本文都归结为GPU Process中。 只要计算机中与图像显示有关的内容,无论是网页,视频,桌面等等都离不开GPU的参与。本文只介绍参与浏览器的部分。
1、合成
浏览器知道了文档的结构、每个元素的样式、页面的几何形状以及绘制顺序,将这些内容展示到屏幕的每一个像素点的过程叫做光栅化。Chrome 在首次发布时采用的是先光栅化视口,当操作例如滚动条时才会光栅化其他的部分。然而,现代浏览器运行一个更复杂的过程,称为合成。
合成是一种技术,可以将页面的各个部分分成图层,把每一层分别将它们光栅化,然后在称为合成器线程的单独线程中合成为页面。如果发生滚动,由于图层已经被光栅化,它所要做的就是合成一个新的帧。可以通过移动图层并合成新帧
2、分层
为了找出哪些元素在哪些层中,主线程遍历布局树来创建层树【Layout Tree】。

除此之外还有被设置为will-change的也会被单独设置为一层。
3、光栅化
一旦创建了层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。合成器线程会光栅化每一层。一个层可能会很大,因此合成器线程将它们分成小块并将每个小块【光栅图像】发送到光栅线程。光栅线程光栅化每个图块,将像素信息转换为GPU可以处理的纹理格式,并将其存储在GPU内存中。
当完成光栅化后,合成器线程会将渲染结果(即合成帧)通过IPC(进程间通信)提交给浏览器进程,
在提交给浏览器进程后,这些合成帧可以被用于更新浏览器UI,例如,网页的刚打开的界面,在输入框中输入了新的内容,那么新的合成帧就会被用来更新这里的文字。
合成的好处是它是在不涉及主线程的情况下完成的。合成器线程不需要等待样式计算或 JavaScript 执行。这就是为什么合成动画【keyframes动画】 被认为是流畅性能的最佳选择。如果需要再次计算布局或绘制,则必须涉及主线程。
至此,最终网页呈现在我们面前。
浏览器对事件的处理简略介绍
浏览器通过对不同事件的处理来满足各种交互需求,这一部分我们一起看看从浏览器的视角,事件是什么,在此我们先主要考虑鼠标事件。在浏览器的看来,用户的所有手势都是输入,鼠标滚动,悬置,点击等等都是。当用户在屏幕上触发诸如 touch 等手势时,首先收到手势信息的是 Browser process, 不过 Browser process 只会感知到在哪里发生了手势,对 tab 内内容的处理是还是由渲染进程控制的。Browser process会发送相应的事件类型以及坐标给Render process,然后Render process会找到对应的的事件对象,并执行该对象上的相关事件处理函数。
总结:
网页优化的极致无非是打开要快、操作流畅、运行稳定。我们在浏览器进程中的网络线程那里介绍了网页打开要快 的解决方案以及原理,在渲染进程中的绘制部分介绍了为了操作流畅要尽量减少回流的操作以及原理,最后的运行稳定需要服务端的配合。
对于出现新的技术希望大家能有自己的认知,例如无论后续出现什么打包工具,他最终影响的是网络线程的部分,也就是网页打开的更快。出现了新的前端框架对于浏览器渲染原理【不会出现变革的情况下】来说,不会有什么影响,它最终还是得转换为html、css、js在渲染进程中作为渲染的数据。
希望看完本文能对前端的理解有新的认识。