浏览器加载和渲染HTML的过程(标准定义的过程以及现代浏览器的优化)
先看一下标准定义的浏览器渲染过程(网上找的):
浏览器打开网页的过程
- 用户第一次访问网址,浏览器向服务器发出请求,服务器返回html文件;
- 浏览器开始载入html代码,发现 head 标签内有一个 link 标签引用外部CSS或JS文件;
- 浏览器又发出CSS及JS文件的请求,服务器返回这个CSS,JS文件;
- 浏览器继续载入html中 body 部分的代码,并且CSS,JS文件已经拿到手了,可以开始渲染页面了;
- 浏览器在代码中发现一个 img 标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码;
- 服务器返回图片文件,由于图片占用了一定面积,影响了页面布局,因此浏览器需要回过头来重新渲染这部分代码;
- 浏览器发现了一个包含一行Javascript代码的 script 标签,赶快执行它;
- Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个 div (style.display=”none”)。杯具啊,突然就少了这么一个元素,浏览器不得不重新渲染这部分代码;
- 终于等到了 html 的到来,浏览器泪流满面……
浏览器加载和渲染html的顺序
- IE浏览器下载的顺序是从上到下,渲染的顺序也是从上到下,下载和渲染是同时进行的。
- 在渲染到页面的某一部分时,其上面的所有部分都已经下载完成(并不是说所有相关联的元素都已经下载完)
- 如果遇到语义解释性的标签嵌入文件(JS脚本,CSS样式),那么此时IE的下载过程会启用单独连接进行下载。
- 并且在下载后进行解析,解析过程中,停止页面所有往下元素的下载,阻塞加载。
- 样式表在下载完成后,将和以前下载的所有样式表一起进行解析,解析完成后,将对此前所有元素(含以前已经渲染的)重新进行渲染。
- JS、CSS中如有重定义,后定义函数将覆盖前定义函数。
JS的加载
- 不能并行下载和解析(阻塞下载)
- web的模式是同步的,开发者希望解析到一个script标签时立即解析执行脚本,并阻塞文档的解析直到脚本执行完;如果脚本是外引的,当引用了JS的时候,浏览器发送一个js request就会一直等待该request的返回,这个过程也是同步的,会阻塞文档的解析直到资源被请求到。因为浏览器需要一个稳定的DOM树结构,而JS中很有可能有代码直接改变了DOM树结构,比如使用 document.write 或 appendChild,甚至是直接使用的location.href进行跳转,浏览器为了防止出现JS修改DOM树,需要重新构建DOM树的情况,所以就会阻塞其他的下载和呈现。这个模式保持了很多年,并且在html4及html5中都特别指定了。开发者可以将脚本标识为defer,以使其不阻塞文档解析,并在文档解析结束后执行。Html5增加了标记脚本为异步的选项,以使脚本的解析执行使用另一个线程。
这里面有几个点需要说明一下:
1.我们知道浏览器的处理过程是解析html生成DOM tree->根据DOM tree和样式表生成render tree->渲染render tree展示。浏览器为了让用户更快的看到页面,所以是边解析html生成局部的DOM tree,浏览器就生成部分render tree然后展示出来。
2.此过程中有两种外部资源是阻塞脚本执行,从而阻塞渲染的,分别是外部js和外部css。外部js是阻塞了DOM tree的生成,因为浏览器需要一个稳固的DOM tree,而js可能破坏这个结构(当然其中也可能会更改样式【注意是样式而不是样式表】,但是这个不阻塞也不会有影响的);外部css样式表也会阻塞脚本的执行,理论上,既然样式表不改变Dom树,也就没有必要停下文档的解析等待它们,然而,存在一个问题,脚本可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题,这看起来是个边缘情况,但确实很常见。Firefox在存在样式表还在加载和解析时阻塞所有的脚本,而Chrome只在当脚本试图访问某些可能被未加载的样式表所影响的特定的样式属性时才阻塞这些脚本。
3.其他外部资源是不阻塞渲染的,比如图片,我们能看到很多时候页面大体的框架都呈现出来了,就是图片的位置没有显示出来的情况,等到图片下载下来以后再重新渲染。
现代浏览器的优化:
按照标准的浏览器渲染和下载过程。下面的代码加载外部资源的顺序应该和资源在html中的顺序一致。其中head中添加了一个外部资源请求http://hm.baidu.com/hm.js?a041a0f4ff93aef6aa83f34134331a1d应该在所有样式之前加载
<html> <head> ... <!--百度统计代码--> <script> var _hmt = _hmt || []; (function() { var host=document.location.hostname; if(/lcfarm.com$/ig.test(host)){ var hm = document.createElement("script"); hm.src = "//hm.baidu.com/hm.js?a041a0f4ff93aef6aa83f34134331a1d"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); } })(); </script> <link rel="stylesheet" type="text/css" href="//static.lcfarm.com/pc-dist/pkg/index.html_aio_f9db6a6.css"> <link rel="stylesheet" type="text/css" href="//static.lcfarm.com/pc-dist/common/css/common_530eedd.css"> <link rel="stylesheet" type="text/css" href="//static.lcfarm.com/pc-dist/css/index_8b620da.css"> <link rel="stylesheet" type="text/css" href="//static.lcfarm.com/pc-dist/pkg/index.html_aio_2_2379650.css"> </head> <body> ... <script type="text/javascript" data-loader="" src="//static.lcfarm.com/pc-dist/common/dep/mod_36ad799.js"></script> <script type="text/javascript" data-loader="" src="//static.lcfarm.com/pc-dist/common/dep/jquery_c07f226.js"></script> <script type="text/javascript" src="//static.lcfarm.com/pc-dist/common/js/jquery/jquery.cookie_546183c.js"></script> <script type="text/javascript" src="//static.lcfarm.com/pc-dist/pkg/common_85ea494.js"></script> <script type="text/javascript" src="//static.lcfarm.com/pc-dist/pkg/index.html_aio_350303c.js"></script> </body> </html>
但是实际上在chrome。Firefox、ie8+等浏览器中却发现是如下效果(使用https://www.webpagetest.org/测试)
为什么?这就是预解析(Speculative parsing)
Webkit和Firefox都做了这个优化,当执行脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变Dom树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。
如上面的那张图,可以看出在执行脚本的时候与解析了一大堆的外部资源引用,并启动线程下载他们,主线程还在等待hm.js的返回。