前言
如图所示,webkit内核浏览器的渲染过程(解析HTML,构建DOM树,解析CSS,构建CSSOM树 ,构建render树,布局layout,绘制painting),这些过程理解起来可能有些抽象,今天我们一起通过chrome开发者工具来直观的理解一下浏览器渲染页面的过程。
页面渲染过程
Performance工具
我们将通过Performance工具来分析页面渲染过程,首先按Command+ Option+ I(Mac)或 Control+ Shift+ I(Windows,Linux)打开DevTools,然后打开performance工具的界面。
我们可以看到Performance 的默认引导页面:
- 第一句提示语所对应的操作是立即开始记录当前页面发生的所有事件,点击停止按钮才会停止记录。
- 第二句对应的操作则是重载页面并记录事件,工具会自动在页面加载完毕处于可交互状态时停止记录。
两者最终都会生成报告(生成报告需要大量运算,会花费一些时间)。我们以实际前端项目(vue项目)为例,点刷新按钮重载页面并记录事件,得到如下报告。
网络请求
这里提到网络请求,是因为浏览器渲染页面离不开网络请求,浏览器渲染进程开始接收HTML数据的标志,其实是收到了浏览器的导航的确认信息,浏览器的导航过程主要是在网络层,涉及DNS解析,浏览器强缓存/协商缓存,TCP三次握手建立连接,http请求响应,TCP四次挥手断开连接等多个环节,这里可以借助performance工具来理解浏览器渲染页面之前的网络请求部分:
上图中,可看到Network模块中有不同的颜色请求,它们分别代表的是蓝色-HTML、黄色-JS、绿色-图片的资源请求,如果有CSS文件的请求的话,它会被标识为紫色。另外一个细节点是,图中每个资源请求的左上角都有一个小方块,深蓝色的小方块代表较高优先级的请求,浅蓝色则代表较低优先级,很明显优先请求的资源(html、webpack打包的manifest、vendor、app主文件请求)都标识了深蓝色的小方块。
我们再来看Event log中浏览器的活动:
- 请求html
- 接收响应头
- 浏览器接收到文档,开始解析处理
- html响应数据已被接收
- 网络请求完成
另外,图中可看到在浏览器发送请求之前,还做了一系列的事情,这是因为我们上面选择重载页面记录事件,所以在send requerst请求Html之前,会触发浏览器一系列默认事件行为:webkitvisibilitychange,unloadEventStart,unloadEventEnd等。
Parse HTML
在浏览器渲染引擎内部,有一个叫HTML 解析器(HTMLParser)的模块,它负责将HTML字节流转换为DOM结构。HTML Standard规范定义了浏览器渲染HTML为DOM的方法。
- HTML解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML解析器就解析多少数据。
浏览器收到响应之后,我们来看下浏览器拿到的html文本
<!DOCTYPE html>
<html><head><meta charset=utf-8><title></title><link rel=dns-prefetch href=//s1.zhuanstatic.com>...<meta name=description content=转转><meta name=viewport content="width=device-width,viewport-fit=cover,initial-scale=1,maximum-scale=1,user-scalable=no"><meta content="telephone=no,email=no" name=format-detection><meta name=apple-mobile-web-app-capable content=yes><meta name=apple-mobile-web-app-status-bar-style content=default>
</head><body><div id=app></div><script type=text/javascriptsrc=https://s1.zhuanstatic.com/u/bmmain/static/js/manifest.f741ad8a3e48f84ad413.js></script><script type=text/javascriptsrc=https://s1.zhuanstatic.com/u/bmmain/static/js/vendor.6858ed31f5c34c2f6f8e.js></script><script type=text/javascript src=https://s1.zhuanstatic.com/u/bmmain/static/js/app.9ff176b78d7af021c2b0.js></script>
</body></html>
再来观察Event Log中浏览器第一次解析HTML的活动:解析html文本,遇到<body>
中的<script>
标签,发送了三个请求,分别请求manifest.js,vendor.js,app.js文件
JS的下载与执行
上图显示,解析HTML的过程中遇到 <script>
标签时,渲染进程会停止解析HTML,而去加载,解析和执行js代码,当脚本执行完成之后,HTML解析器才会恢复解析过程,而js外链资源的加载,解析和执行通常又会很耗时,这就是前端常提及到的js阻塞的原因。当然,浏览器设置停止解析HTML的机制也是有原因的,因为js可能会改变DOM的结构(例如使用document.write等API)。不过我们也有多种方式来告知浏览器应对如何应对某个资源,例如在<script>
标签上添加了 async
或 defer
等属性,浏览器会异步的加载和执行JS代码,而不会阻塞渲染。
加载次优先级的资源
html页面中有CSS,JS,字体等额外的资源,这些资源也需要从网络上或者浏览器缓存中获取。主进程可以在构建 DOM 的过程中会逐一请求它们,为了加速,浏览器的预加载扫描器会同时运行,如果在 html 中存在 <img>
<link>
等标签,预加载扫描器会把这些请求传递给浏览器进程中的网络线程进行相关资源的下载。
构建DOM树
为什么浏览器要构建DOM树?这是因为浏览器无法直接理解和使用HTML文本,需要将HTML转换为浏览器能够理解的结构—DOM树。为了更加直观地理解DOM树,我们可以打开Chrome的“开发者工具”,选择“Console”标签来打开控制台,然后在控制台里面输入“document”后回车,就能看到一个完整的DOM树结构,如下图所示:
DOM和HTML内容几乎是一样的,但是和HTML不同的是,DOM是保存在内存中树状结构,可以通过JavaScript来查询或修改其内容。
构建CSSOM
为什么浏览器要构建CSSOM树?原因和浏览器构建DOM树一样,也是无法直接理解这些纯文本的CSS样式,所以当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构——styleSheets。我们可以在Chrome控制台中查看其结构,只需要在控制台中输入document.styleSheets,然后就看到如下图所示的结构
样式计算
渲染进程主线程计算每一个元素节点的最终样式值,即使没有任何CSS样式,浏览器对每个元素也会有一个默认的样式。我们也可以通过Chrome查看浏览器计算后的样式
生成布局树
为了构建布局树Render tree,浏览器大体上完成了下面这些工作
- 遍历DOM树中的所有可见节点,并把这些节点加到布局(Layout)中;
- 而不可见的节点会被布局树忽略掉,如head标签下面的全部内容,再比如body.p.span这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。
回到Event Log继续观察分析,如下图,js引擎在彻底加载执行完毕vendor.js,manifest.js,app.js后,渲染引擎回归,开始第三次解析Html,开始进行首次样式计算和布局,并动态加载manifest映射中的app-async.js等(项目webpack打包split chunk优化后的产物)和首页correlate-Hplan.js资源等。
绘制
即使知道了不同元素的位置及样式信息,浏览器还需要知道不同元素的绘制先后顺序才能正确绘制出整个页面。在绘制阶段,主线程会遍历布局树创建待绘制列表。打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表
绘制过程可以将布局树中的元素分解为多个层。
回到performance面板,我们勾选上控制面板里的 Enable advanced paint instrumentation 记录渲染事件的细节:选择Frames中的一块,可在“Event log” 选项卡旁边的新“Layers”选项卡中显示有关页面分层的信息。
将内容提升到GPU上的层(而不是CPU上的主线程)可以提高绘制和重新绘制性能。有一些特定的属性和元素可以实例化一个层,包括<video>
和<canvas>
,任何CSS属性为opacity、3D转换、will-change的元素,还有一些其他元素。这些节点将与子节点一起绘制到它们自己的层上,除非子节点由于上述一个(或多个)原因需要自己的层。
层确实可以提高性能,但是它以内存管理为代价,因此不应作为web性能优化策略的一部分过度使用。
继续来观察这个阶段的Event Log的活动,刚刚分析到浏览器第三次解析html,动态加载完webpack拆分的chunk和首页js,js引擎执行完毕触发Event load事件,紧接着触发了domInteractive事件也标识了html解析结束,html解析器结束工作后触发docmument.readystatechange事件,紧接着触发DomContentLoaded事件后,更新布局树,进行首次绘制。
这里我们再来理解下这个DomContentLoaded事件与Load事件的区别
HTML 页面的生命周期包含三个重要事件:
- DOMContentLoaded —— 浏览器已完全加载 HTML,并构建了 DOM 树,但像 和样式表之类的外部资源可能尚未加载完成。
- load —— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。
- beforeunload/unload —— 当用户正在离开页面时。
我们来看Timings模块,可以看到DCL,FP,FCP,L,LCP几个重要的与页面性能优化密切相关的事件缩写,这里我们只理解下DCL(DOMContentLoaded)和L(load),load事件触发的时机明显晚于DOMContentLoaded
合成与显示
一旦层树被创建,绘制列表被确定,主线程会把这些信息通知给合成器线程,合成器线程会栅格化每一层。有的层的可以达到整个页面的大小,因此,合成器线程将它们分成多个磁贴,并将每个磁贴发送到栅格线程,栅格线程会栅格化每一个磁贴并存储在 GPU 显存中。合成线程发送绘制图块命令DrawQuad给浏览器进程。浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。
回到performance工具,这里,我们滑动缩小概览模块选中的区间,定位到页面骨架屏与初次渲染出页面的中间,在Event Log中只勾选Painting会自动筛选出区间范围内的绘制事件日志,可以看到浏览器进行了多次绘制和合成图层的操作。
待优化的性能问题
额,这部分本来没有在计划范围内 ,主要是两个问题被chrome⚠️了:
- Long task多个主任务都花费了较长的时间被警告,主要是打包后js的体积大造成的
- Layout Shift布局偏移过大,导致用户体验差,简单点理解就是你准备点一个链接或者按钮,但就在你手指触摸到屏幕的前一秒,链接移动了,你点到了其他地方
总结
通过performance工具的Event Log模块,分析了一下转转某实际vue项目 SPA单页应用页面在浏览器中的渲染过程,过程中可能有理解不到位的地方,欢迎多多指出呀。
参考文献
[1].https://blog.poetries.top/browser-working-principle/guide/part1/lesson05.html
[2].https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference
[3].https://developer.mozilla.org/zh-CN/docs/Web/Performance/%E6%B5%8F%E8%A7%88%E5%99%A8%E6%B8%B2%E6%9F%93%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86
[4].https://developers.google.com/web/updates/2018/09/inside-browser-part3
[5].https://zhuanlan.zhihu.com/p/41017888