现在让我们看看浏览器从网络上加载资源所耗费的时间(我们忽略从缓存以及从CDN等中间商网络上加载资源),我们首先要知道的是:
- 一个到无服务的网路往返 (传播延迟) 大约100ms
- 服务器对于HTML文档的响应大约100ms,对于其他资源大约10ms
The Hello World experience
<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>Critical Path: No Style</title></head><body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div></body> </html>
我们只是加载了时间和网页——没有任何的js和css资源等——我们的页面非常简单,下面我们看看谷歌浏览器开发者工具里记录下载用的时间:
HTML文档大约花费200ms来加载。注意上面的蓝色透明进度条表明了网路上的一个往返,这里我们的HTML文档非常的小(小于4k),我们只是进行了简单的文件抓取工作,一半的时间来等待网络响应,一般的时间花费在服务器响应上,总用时大于200ms。
一旦HTML文档内容可以使用,浏览器开始解析HTML内容(全部是字节),将其转化成tokens,然后建立DOM树。注意在工具条最下面显示了DOMContentLoaded事件花费的时间是216毫秒,也就是对应的蓝色垂直实线。在蓝色透明进度条和蓝色垂直实线之间的时间就是浏览器建立DOM树的时间——在上例中只是花费了几毫秒。
注意我们的“awesome photo”没有阻塞任何的DOMContentLoaded事件,所以我们可以直接构造render tree甚至将内容显示在屏幕上,不用等待图片资源:不是所有的资源都会阻塞渲染!所以,在谈论渲染过程的时候,我们只是谈论了js,css和html标记。图片虽然不会阻塞渲染,但是我们仍旧应该让其迅速加载显示。
但是,“load”事件(也被叫做"onload")事件却被图片加载阻塞:因为load事件是在所有资源加载完毕之后触发,上图中也即是最后的红色垂直实线,也就是工具条最下面显示的load:335ms,这也是浏览器加载器停止加载的时间点。
Adding JavaScript and CSS into the mix
实际的编程中肯定需要css和js,所以让我们在其中加入css和js:
<html><head><title>Critical Path: Measure Script</title><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css" rel="stylesheet"></head><body onload="measureCRP()"><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="timing.js"></script></body> </html>
在加入js和css之前:
在加入js和css之后
css文件和js文件都在异步同时被加载,这是好事!注意load和DOMContentLoaded和之前的微妙的不同:
- 不像只是简单的HTML文件(不包含任何的css和js),包含js和css的文件还需要将css编程CSSOM树,以及CSSOM树和HTML树相互结合形成render tree的过程。
- 因为js文件可能要处理CSSOM,所以css文件必须在执行js文件之前执行!并且css的下载将会阻塞domContentLoaded事件。
如果我们将js直接写在html内部呢?
外部js导入:
直接书写js:
虽然我们减少了一次加载请求,但是事件是差不多的,因为在解析script标签的时候将会阻塞直到CSSOM被创建完成。同时,从上图中我们可以看见,css和js文件的加载是平行进行的。
所以,我们有什么策略来减少事件呢?
让我们在js文件中加入async异步关键字:
<html><head><title>Critical Path: Measure Async</title><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css" rel="stylesheet"></head><body onload="measureCRP()"><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script async src="timing.js"></script></body> </html>
同步内部加载js:
异步外部加载js:
可以看见domContentLoade事件在HTML解析完成后马上执行:浏览器知道了不要在处理js中对其进行阻塞,因为没有了其他渲染的阻塞,CSSOM构造可以同时进行。
我们可以同时在文件内部书写js和css文件内容,而不是外部引入:
<html><head><title>Critical Path: Measure Inlined</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>p { font-weight: bold }span { color: red }p span { display: none }img { float: right }</style></head><body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script>var span = document.getElementsByTagName('span')[0];span.textContent = 'interactive'; // change DOM text content span.style.display = 'inline'; // change CSSOM property// create a new element, style it, and append it to the DOMvar loadTime = document.createElement('div');loadTime.textContent = 'You loaded this page on: ' + new Date();loadTime.style.color = 'blue';document.body.appendChild(loadTime);</script></body> </html>
可以看见domContentLoaded事件的时间和异步加载外部js文件例子中个domContentLoadedNotice时间差不多。
将css和js文件内容直接书写在html文件内部,虽然会让html变得非常庞大,但是可以减少外部资源的加载。
优化模式
最简单的html就是没有css和js亦或依他资源,只有html内容。
渲染这种页面,浏览器你需要初始化请求,等待html文档加载完成,解析它,建立DOM树,最后渲染到屏幕上:
<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>Critical Path: No Style</title></head><body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div></body> </html>
T0 和T1 之间的时间捕捉了网络和服务器操作的时间。
在理想情况下(如果HTML文档足够小),所有我们需要做的就是使用一个网络往返来获取整个文件——根据TCP传输协议,文件越大需要越多的网络往返。所以上图是建立在最理想的状况下,加载文件只要一个网络往返。
让我们增加css文件:
<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css" rel="stylesheet"></head><body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div></body> </html>
我们使用了一个网络往返来获取HTML资源,在将内容最终渲染到屏幕上之前我们必须要加载css文件,所以我们使用了一个网络往返来获取css文件,最后渲染页面。注意我们这里每次获取资源使用一个网络往返是建立在资源非常小的情况下,也就是说最理想的状态下。
让我们使用关键术语来描述渲染过程:
- 关键资源: 资源可能会阻塞页面的渲染(这是使用可能是因为图片等资源是不会阻塞页面的渲染的,只有css和js资源会)
- 渲染路径长度: 网络往返的次数,亦或获取所有资源的总时间 number of roundtrips, or the total time required to fetch all of the critical resources.
- 关键字节: 页面第一次渲染时所需要的字节数,也就是将关键资源文件大小的总和。
- 我们上面第一个最简单的网页形式的例子,就包含了一个关键资源(HTML文档),关键路径长度是1个网络往返(假设文件非常小),总共的关键字节是HTML文档的字节大小。
让我们比较上面HTML+CSS文件的渲染过程:
- 2 关键文件:CSS文件和HTML文档
- 2 或者更多的路径长度:加载css和HTML时所分别需要一个网络往返
- 9 KB 的关键字节:2个关键文件的总和
现在让我们加入js:
<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css" rel="stylesheet"></head><body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js"></script></body> </html>
过程是,加载完了HTML之后,同时加载css和js文件,因为js要查询CSSOM,所以必须等到cssom建立完成知乎才可以运行js,之后建立DOM,最后渲染页面。
- 3 个关键文件:CSS和JS和HTML文件分别一个
- 2 或者更多的路径长度:因为加载css和js是同时进行的,也就是说在同一个网络往返中,加载HTML进行了一次网络往返
- 11 KB 的关键字节:3个关键文件的大小综合
异步加载js文件:
<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css" rel="stylesheet"></head><body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js" async></script></body> </html>
异步加载js的好处:
- js脚本不会让浏览器在解析其他文件时对其进行阻塞,也不会影响关键渲染进程
- 因为关键文件中不再包含js脚本文件,css同样不需要阻塞domContentLoaded事件
- domContentLoaded事件越快被触发,其他的应用逻辑也会越快被执行
所以,最后的结果是我们只有两个关键文件,且我们只有两个路径长度(因为我们只进行了两次网路往返),并且9kb的关键字节。
如果css只是针对打印机设备呢?
<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css" rel="stylesheet" media="print"></head><body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js" async></script></body> </html>
因为css资源只是用于打印机设备,所以浏览器不需要下载css时进行渲染阻塞,所以,一旦DOM构造完成,浏览器就有足够的信息来渲染页面!
所以,文档只有一个关键文件(HTML文档),最小的渲染路径(只有一个网络往返)。
原文:https://developers.google.com/web/fundamentals/performance/critical-rendering-path/analyzing-crp?hl=en