网页生命周期与浏览器渲染过程
coconilu opened this issue · 0 comments
访问一个网页并观察它的生命周期
浏览网页是最普通不过的操作。
当我们输入URL并点击回车,或者点击某个跳转链接,浏览器是怎么导航到目标网页的呢?
随便打开一个网页,然后在控制台里输入:
performance.timing
performance.timing
并不是标准,现在推荐使用performance.getEntriesByType("navigation")
,这两个接口相差不大,后面的主要是相对开始导航时间戳。
你将会看到一个对象,它的key列表如下:
navigationStart,开始导航
unloadEventStart,开始卸载上一个页面
unloadEventEnd,完成卸载上一个页面
redirectStart,开始重定向
redirectEnd,结束重定向
fetchStart,请求开始
domainLookupStart,域名解析开始
domainLookupEnd,域名解析结束
connectStart,开始建立连接
connectEnd,完成建立连接
secureConnectionStart,开始建立安全连接
requestStart,请求发出
responseStart,接收到响应
responseEnd,接收全部响应
domLoading,当前网页DOM结构开始解析时,Document.readyState属性变为“loading”
domInteractive,HTML加载和解析完成,DOM树构建完成(可交互),开始加载内嵌资源,Document.readyState属性变为“interactive”
domContentLoadedEventStart,注册的(DOMContentLoaded)事件处理器开始执行,此时DOM树和CSSOM树构建完成(渲染树构建完成)
domContentLoadedEventEnd,所有事件处理器执行完毕
domComplete,当前文档解析完成并加载完所有的子资源,即Document.readyState 变为 'complete'
loadEventStart,页面资源加载完毕,触发onload事件
loadEventEnd,onload事件执行完毕
用一张图表示上面的事件:
以上就是网页的生命周期,先是解析URL拿到目标IP,然后去服务器拉取静态资源(HTML、CSS、JS),然后开始解析静态资源和执行JS,最后渲染出一个页面。
了解了这些,我们大概可以知道网页性能优化的点。比如域名解析,chrome浏览器自己会缓存DNS解析结果,也就是说并不是每次访问网页都会进行DNS解析。还有对于HTTP协议,HTTP1.x需要用多个TCP连接(chrome对同一个域名最多只能建立6个TCP连接)来提升请求资源效率,如果升级到HTTP2,就可以多路复用一个TCP连接,提升资源传输效率。
Chrome浏览器架构演化
早期的浏览器是单进程架构,也就是所有页面的渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中的,会存在如下的问题:
- 不稳定,渲染过程、插件出错都会导致整个浏览器崩溃
- 不流畅,脚本或者插件会让单进程浏览器变卡顿
- 不安全,脚本可以通过浏览器的漏洞来获取系统权限,进而攻击用户电脑
目前的浏览器都是多进程架构:
- 浏览器进程,主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程,核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程,其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 网络进程,主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程,主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
浏览器渲染过程
浏览器是如何把静态资源(HTML、CSS、JS)整合渲染成页面的呢?
Chrome浏览器基于Blink渲染引擎绘制页面,渲染流程大致如下:
- 主线程(Main Thread) 解析HTML、计算Style,得出元素的布局信息,包括Parse HTML、Recalc Styles、Layout三个过程
- 分层,并计算图层信息(图层树),拥有层叠上下文属性的元素会被提升为单独的一层,需要剪裁(clip)的地方也会被创建为图层
- 计算得出每一个图层的绘制指令,并把这些指令发送给合成线程(Compositor Thread),由合成线程安排栅格化图层并生成位图
- 合成线程通知GPU进程根据位图绘制页面
可以通过devtools的Layers标签查看相关分层信息。明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。也可以使用CPU来栅格化,合成线程会spawn出一个或多个Compositor Tile Worker Thread,然后多线程并行执行栅格化操作
至此,一个页面就展示出来了。那么当用户进行某些操作后,如何更新视图的呢?
帧的生命周期
更新视图涉及到帧的概念。
查看网页帧的信息,可以通过打开devtools里的Rendering,并勾选FPS meter,就可以看到网页的左上角悬挂一个小窗口,里面显示当前的帧率,和GPU的相关信息。
浏览器会尽可能不去绘制帧,也就是说,如果没有用户操作,那么帧率会变低。
下图是真的生命周期:
- 新的一帧开始
- Input event handlers之前Compositor Thread接收到的用户UI交互输入在这一刻会被传入给主线程,触发相关event的回调(包括但不限于:touch event、input event、wheel event、click event)。
- 执行 requestAnimationFrame 回调
- parse HTML,如果有DOM变动,那么会有解析DOM的这一过程
- Recalc Styles,如果你在JS执行过程中修改了样式或者改动了DOM,那么便会执行这一步,重新计算指定元素及其子元素的样式
- Layout,如果有涉及元素位置信息的DOM改动或者样式改动,那么浏览器会重新计算所有元素的位置、尺寸信息。而单纯修改color、background等等则不会触发重排
- Update layer tree,更新Render Layer的层叠排序关系
- Paint,计算得出更新图层的绘制指令
- Composite,把绘制指令传到合成线程
- 此时如果主线程(Main Thread)在下一帧到来之前还有时间的话,会执行requestIdleCallback回调
- 合成线程会安排栅格化操作并通知GPU进程刷新这一帧
可以看出来,帧的生命周期和网页渲染过程是差不多的。
参考
Navigation Timing
Navigation Timing API
[译] 浏览器帧原理剖析
极客时间——李兵的《浏览器工作原理与实践》