一、HTML支持的组要资源类型javascript
在浏览器内核有一个管理资源的对象CachedResource类,在CachedResource类下有不少子类来分工不一样的资源管理,这些资源管理子类分别是:css
资源 | 资源管理类 |
HTML | MainResource ===> CachedRawResource |
JavaScript | CachedScript |
CSS | CachedCSStyleSheet |
图片 | CachedImage |
SVG | CachedSVGDocument |
CSS Shader | CachedShader |
视频、音频、字幕 | CachedTextTrack |
字体文件 | CachedFont |
XSL样式表 | CachedXSLStyleSheet |
二、资源缓存html
资源的缓存机制是提升资源使用效率的有效方法。基本思想就是创建一个资源缓存池,当web须要请求资源时,会先从资源池中查找是否存在相应的资源,若是有的话就直接取缓存,若是没有就建立一个新的CachedResource子类的对象,并发送请求给服务器(由网络模块完成),请求回来的资源会被添加到资源池,而且将资源(数据信息:好比在资源池中的物理地址)设置到该资源的对象中去,以便下次使用。vue
下面是一个缩减版的资源请求原理图:java
实质上的操做是在资源对象中找到对应资源的物理地址(url),而后返回给渲染引擎,渲染引擎在渲染页面时根据url获取物理内存中的资源数据。因为资源的惟一特性是url,因此当两个资源有不一样的url,可是他们的内容彻底相同时,也不会被认定是同一个资源。web
注:这里所说的缓存是内存,不是磁盘。算法
三、资源加载器express
在WebKit中共有三种类型的资源加载器,分别是:bootstrap
3.1针对每种资源类型的特定加载器,用来加载某一类资源。例如“image”这个元素,该元素须要图片资源,对应的顶资源加载器是ImageLoader类。浏览器
3.2资源缓存机制的资源加载器,特色是全部特定加载器都共享它来查找并插入缓存资源——CachedResourceLoader类。特定加载器是经过缓存机制的资源加载器来查找是否有缓存资源,它属于HTML的文档对象。
3.3通用的资源加载器——ResourceLoader类,是在WebKit须要从网络或者文件系统获取资源的时候使用该类只负责得到资源的数据,所以被全部特定资源加载器所共享,它属于CachedResource类,与CachedResourceLoader类没有继承关系。
若是说资源缓存和网络资源是浏览器要渲染页面的资源实体,那资源加载器就是为浏览器实现页面渲染提供资源数据的搬运工。前面的资源请求至关于就是资源地址寻址的过程,真正为渲染提供资源的过程是下面这样的:
这个资源加载看起来很复杂,可是模块分工很明确,基于资源对象与内存资源缓存的对应关系(每一个缓存资源在资源对象上有一个实例),当浏览器触发资源请求时先经过判断资源是否有缓存资源,若是有的话就就直接拿缓存资源给渲染引擎,若是没有就经过网络请求获取资源给渲染引擎,而且同时会将资源缓存到内存中。
同CachedResourceLoader对象同样,资源池也属于HTML文档对象,因此资源池不能无限大,对于资源容量不能无限大的问题浏览器的解决方法有两种:第一种是采用LRU(Least Recent Rsed最近最少使用原则)算法。第二种方法是经过HTTP协议决定是否缓存,缓存多久,以及何时更新缓存,而后咱们开发时还可决定资源如何拆分,拆分可让我决定哪些资源缓存,哪些资源不缓存。
当请求协议指定能够取缓存数据,请求资源会先判断内存中是否有资源,而后将资源的信息(版本,缓存时常等)经过HTTP报文一块儿发送给服务器,服务器经过报文判断缓存的资源是不是最新的,资源缓存是否超时来决定是否从新获取服务端的资源,若是不须要从新获取服务端的资源,服务器会返回状态码304,告诉浏览器取本地缓存资源。
下面经过Chrome浏览器来请求饿了吗官网,在控制台查看数据请求的资源加载过程,而且经过刷新页面查看当页面刷新时浏览器在缓存中取了哪些信息:
接着咱们再来刷新页面看看取了哪些缓存数据:
能够看到饿了吗官网的缓存机制是将document主文件和js文件作了缓存处理。这样的处理方式能够很大程度上提升页面性能和下降服务器请求压力,至于为何就是接下来的内容了。
前面介绍了浏览器资源请求与资源加载的基本原理,看上去好像是一个简单的线性步骤,可是实质上浏览器内部是多进程异步加载这些资源的,咱们知道网页的效果是基于DOM结构和CSS样式表来完成基本的页面效果呈现,可是JS代码又能够对DOM节点进行增删该查操做,还能够修改DOM的CSS样式,那必然就是须要先有DOM结构,而后添加CSS样式,再就这两个资源的基础经过JS修改后才能呈现出来,可是何时加载(指的是下载资源,并非前面的资源加载到页面上的整个过程)?何时执行?何时渲染页面?按照什么规则来完成这些工做呢。
一般咱们给某个服务器发送一个web请求时,首先返回的是一个HTML资源。假设这个资源的内部代码以下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <link rel="stylesheet" type="text/css" href=".../css/xxx.css"> </head> <body> <div> <p> <span></span> </p> <ul> <li><img src=".../image/xxx.png" alt=""></li> <li><img src=".../image/xxx.png" alt=""></li> <li><img src=".../image/xxx.png" alt=""></li> </ul> </div> <script src=".../javascripts/xxx.js" type="text/javascript"></script> </body> </html>
本地获取到了HTML资源后第一步就是解析HTML,也就是常说的DOM解析,首先是建立一个document对象,而后经过DOM解析出每一个节点。经过DOM解析发现页面中有css外部样式表须要加载,就当即经过CSS加载器执行加载。解析到img元素发现须要加载图片,就当即经过图片加载器执行加载,这个过程不会等待前面加载的资源加载完成才启动第二个加载,而是经过异步的方法开启多个加载线程,而且浏览器底层会开启多个进程来处理这些线程(Chrome会开启五个进程)。一样解析到了script元素时发现须要外部js资源会当即加载js文件资源。
深度优先原则解析构建DOM树和CSS树:
深度优先原则就是对每个结构顺着第一个内部节点一直往内部解析,直到结构尽头,而后再回退到上一个节点,再对第二个节点执行深刻优先原则的解析构建。下图是上面示例请求到的HTML资源的解析流程图:
按照示例HTML解析流程图,根据编号顺序按照1-->1.1-->1.2-->1.3-->1.4-->2-->2.1-->2.1.1-->2.1.1.1-->2.1.2-->2.1.2.-->2.1.2.1-->2.1.2.2-->2.1.2.3-->2.2。用一句来表达这种解析原则就是一条道走到黑,开玩笑,可是的确很形象哈。CSS样式表解析和构建CSS树也一样使用这个原则。当DOMTree和CSSTree都构建完成之后就会被合并成渲染树(randerTree)。渲染树解析完毕之后就开始绘制页面。
了解了DOMTree和CSSTree的构建原理,而后合成randerTree绘制页面,可是这个过程怎么能缺乏JS呢?有了JS的参与,这个过程就会变得复杂了。首先,CSS资源是异步加载(下载),在CSS资源加载的过程当中,DOM解析会继续执行操做。可是当遇到script标签的时候,若是是外部资源就要当即加载(下载),若是是内部资源就会当即执行JS代码,当即执行JS代码会阻断HTML的解析(由于JS会操做DOM节点增删改查什么的,还会操做元素样式),霸道总裁JS就这样让傻媳妇HTML傻呆着让它随心所欲了。就算是外部JS资源加载(下载)的过程HTML的解析也是被阻断的,这个过程是必须等到JS加载(下载)完,而后还要等他执行完才能继续解析HTML。
<img class="img1" src="https://img.baidu.com/search/img/baidulogo_clarity_80_29.gif" alt="Baidu" align="bottom" border="0"> <script type="text/javascript"> // 循环5秒钟 var n =Number(new Date()); var n2 = Number(new Date()); while((n2 - n) < (10*1000)){ n2 = Number(new Date()); } console.log(document.querySelectorAll(".img1"));//NodeList [img.img1] console.log(document.querySelectorAll(".img2"));//NodeList [] </script> <img class="img2" src="https://gss1.bdstatic.com/9vo3dSag_xI4khGkpoWK1HF6hhy/baike/w%3D268%3Bg%3D0/sign=7aa2c00bdd58ccbf1bbcb23c21e3db03/908fa0ec08fa513defeb0567316d55fbb3fbd9c2.jpg"> <script> var n3 = Number(new Date() - n2); console.log(n3);//13 console.log(document.querySelectorAll(".img1"));//NodeList [img.img1] console.log(document.querySelectorAll(".img2"));//NodeList [img.img2] </script>
由上面的示例能够说明js执行会阻塞DOMTree构建,否则在JS等待的10秒里足够解析一个img元素,可是10秒后只能查询到img1,img2查询不到(打印空DOM节点对象)。当第二次打印的时候两个img节点就都获取到了。接着咱们来看看外部JS加载会不会阻塞DOMTree构建:
<script> var n =Number(new Date()); </script> <!-- 设置网速30kb/s测试js是否阻塞渲染 --> <script src="https://cdn.staticfile.org//vue/2.2.2//vue.min.js"></script> <script> var n3 = Number(new Date() - n); console.log(n3);//30~40秒 ---- 注释外部js加载代码测试时间差为0秒 </script>
测试结果是外部JS的加载也会阻塞HTML解析构建DOMTree。因此结论是JS的加载和执行都会阻塞DOMTree的构建,接着问题又来了,咱们前面提到过JS代码会操做DOM还会操做CSS,因此从理论上讲JS确定得须要等到CSS加载解析完才会执行,CSS阻塞JS执行是确定的,再思考CSS的加载(下载)会阻塞JS的加载(下载)吗?
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Title</title> <link type="text/css" rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" /> <script src="https://cdn.staticfile.org//vue/2.2.2//vue.min.js" type="text/javascript" charset="utf-8" async defer></script> </head> <body> </body> </html>
咱们来看Chrome控制台的时间线:
由Chrome控制台的时间线能够看到外部JS和外部CSS几乎是同时开始加载,CSS加载并无阻塞JS的加载。既然这样咱们再来测试如下CSS加载阻塞JS执行是不是真的?
<script> var n = Number(new Date()); </script> <link type="text/css" rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" /> <script> console.log(Number(new Date()) - n);//外部CSS阻塞JS执行40~200毫秒 --- 注释外部CSS代码测试差值0~1毫秒 </script>
可能有人会疑惑我为何不测试外部CSS会不会阻塞HTML解析,你想一想若是CSS阻塞HTML解析那JS加载必须会被阻塞吧,因此CSS加载也就不会阻塞HTML解析了。可是,CSS会阻塞JS执行,也就间接的阻塞了JS后面的DOM解析。
其实相对来讲JS与CSS阻塞仍是比较好理解的,毕竟还有可参考的数值和可视的图像信息,接下来的问题就只能依靠逻辑推理了。
在阐述JS时间线以前,我另外总结了一部分很是重要的内容:JS的异步加载(JS异步加载的三种方案),JS异步加载与下面的内容相关联的内容比较多,建议在了解下面内容以前先了解一下JS异步加载。
在前面的内容中解析了访问网站获取资源的基本原理,而后资源被访问到本地后怎么解析,解析时发什么的异步资源加载,同步资源加载,同步执行等一系列内容。而后在JS异步加载中提到了script.onload事件、script.onreadystatechange事件、script.readyState状态,而后还有document.readyState="interactive"文档状态和docuement.readyState="complete"文档状态。这些内容都发生在打开网页的那一瞬间,可是这一瞬间不仅是检验物理配置的性能、浏览器内核的性能以及网络的性能,还关系到web开发者基于这些已定的基础平台的代码优化,因此咱们有必要对这整个过程有很是清晰的理解,才能实现友好的程序设计。下面咱们就经过JS时间线来描述这个过程如何发生的:
页面加载的五个步骤和JS时间线的十个环节:
要说是JS时间线的话,可能不是很恰当,或者应该说是文档模型初始化构建过程的JS表示,可以操做DOM对象接口的语言有不少,这里就是用JS来表示DOM对象模型初始化的整个过程。
//readyState属性返回当前文档的状态
uninitialized - 还未开始载入
loading - 载入中
interactive - 已加载,文档与用户能够开始交互
complete - 载入完成--loaded
关于重排/回流(reflow)重绘(repaint)简单来讲就是会将已经计算好的布局和构建好的渲染树(randerTree)从新计算和构建所有或者部分。这部分发生在DOMTree和CSSTree解析完成之后,也就是会发生在构建randerTree时和以后,这里咱们重点关注发生在randerTree构建时的重排/回流和重绘问题,也是网页渲染除了JS、CSS阻塞以后的性能优化区间。
发生重排/回流与重绘其本质上从新布局和构建randerTree,若是将DOM以前的执行过程理解为同步,这个时候浏览器转为事件取动的异步阶段,浏览器内核在构建randerTree的同时JS也会被事件取动参与修改文档的结构和样式,也是触发重排/回流与重绘行为的关键所在,而本质上作的事情就是从新计算布局和构建randerTree树,因此在解析重排与重绘以前先来了解如下布局计算和randerTree构建:
在构建randerTree时并不会把CSS样式表或者行内样式表示元素大小和位置的数据添加到RanderObject上,而是要基于样式设置(如):width、height、font-size、display、left、top、bottun、right还有borde、padding、margin的大小,结合上下文的相互做用(好比有子元素自适应父级元素大小和位置或者父元素基于子元素定义自身大小和位置),最后使用RanderObject上的layout()方法计算出肯定的元素大小和位置,这个过程layout()方法是递归完成整个计算操做。
由于布局计算须要基于元素上下节点来进行,元素的大小和位置变化都有可能会影响到父级和子级的元素大小和位置变化,因此randerTree上的某个RanderObject的相关数据发生变化除了自身的layout()方法须要从新执行计算,还可能会触发上下级的节点的layout()方法的从新执行计算。
因此当构建randerTree的时候由document.onreadystatechange事件、defer的脚本、DOMContentLoaded事件还有不肯定的src异步加载的JS脚本均可能在这时候修改元素的大小和位置,甚至修改DOM结构。
除了脚本的影响外,还有多是浏览器窗口发生产生变化致使全局的randerTree从新布局计算,另外若是脚本修改了全局的样式也一样可能会触发全局的从新布局计算。
有了前面对布局的介绍,重排/回流就一目了然了,当因为脚本执行或者浏览器窗口变化,引起RanderObject上的layout()方法从新计算机布局数据,就叫作重排/回流。从字面上的含义来理解重排很容易,就是因为元素的大小和位置变化页面从新排列布局。回流就存在一些逻辑上的理解了,在布局中由于元素节点的位置和大小是存在上下级和同级之间相互影响的,因此若是有脚本修改DOM节点或者大小位置样式,就会对相关连的元素进行判断查找修改的范围指定修改逻辑,制定layout()方法的递归顺序的最优方案,这个查询判断和修改过程就是须要在节点之间来回操做,这也就是回流。实质上重排/回流说的都是一回事。
重绘不会影响布局,可是当脚本触发了样式修改,而修改的部分是背景(图片和颜色)、字体颜色、边框颜色等,而这些修改也存在嵌套的节点链级相互影响,因此也是须要遍历操做,重绘不至于影响到布局,但也是一个相对损耗性能的操做,毕竟都须要DOM文档和JS引擎结构之间的桥梁通道来执行操做。不太重绘相对于重排来讲就要快的多了。
重排/回流与重绘是会发生在randerTree构造时,也会发生在randerTree构造结束后,都是相对损耗CPU甚至GPU的操做,只是页面首次渲染更值得的咱们关注。
当randerTree构建完成之后就会开始绘制页面了,在绘制页面过程当中仍然可能发生重排与重绘,但这里须要重点关注的是图层合并,绘制主要是基于CPU的计算来实现,同时浏览器基本上都采用GPU加速的混合模式,其实浏览器自己不须要操做图层合并,由于绘图无论是CPU仍是GPU来实现都是基于元素的大小和位置将它们实现的图层,图们自己就在同一个位置,因此无需合并操做。
CPU主要负责randerTree的绘制工做,它与GPU的配合在不一样浏览器内核中会略微不一样,可是在同一个位置出现的图层越多,确定是对性能的损耗就越大。并且因为CPU主要负责randerTree的绘制,多图层就会对GPU带来很大的工做负载,具体包括:CSS3 3D变形、CSS3 3D 变换、WebGL 和 视频。也有浮动,定位,溢出隐藏,z坐标重叠等都是在绘制过程当中比较损耗性能的行为。
最后通过这样艰难的过程事后,网页终于呈如今咱们桌面,可是注意window事件交互不会等待绘制完成,决定window事件交互的是资源是否所有加载完成,这里指的资源是HTML文档包含内容资源,并不包含外部脚本加载的资源。
(减小重排与重绘的一些要点)
1 1:不要经过父级来改变子元素样式,最好直接改变子元素样式,改变子元素样式尽量不要影响父元素和兄弟元素的大小和尺寸 2 2:尽可能经过class来设计元素样式,切忌用style 3 3:实现元素的动画,对于常常要进行回流的组件,要抽离出来,它的position属性应当设为fixed或absolute 4 4:权衡速度的平滑。好比实现一个动画,以1个像素为单位移动这样最平滑,但reflow就会过于频繁,CPU很快就会被彻底占用。若是以3个像素为单位移动就会好不少。 5 5:不要用tables布局的另外一个缘由就是tables中某个元素一旦触发reflow就会致使table里全部的其它元素reflow。在适合用table的场合,能够设置table-layout为auto或fixed, 6 6:这样可让table一行一行的渲染,这种作法也是为了限制reflow的影响范围。 7 7:css里不要有表达式expression 8 8:减小没必要要的 DOM 层级(DOM depth)。改变 DOM 树中的一级会致使全部层级的改变,上至根部,下至被改变节点的子节点。这致使大量时间耗费在执行 reflow 上面。 9 9:避免没必要要的复杂的 CSS 选择器,尤为是后代选择器(descendant selectors),由于为了匹配选择器将耗费更多的 CPU。 10 10: 尽可能不要过多的频繁的去增长,修改,删除元素,由于这可能会频繁的致使页面reflow,能够先把该dom节点抽离到内存中进行复杂的操做而后再display到页面上。 11 11:请求以下值offsetTop, offsetLeft, offsetWidth, offsetHeight,scrollTop/Left/Width/Height,clientTop/Left/Width/Height,浏览器会发生reflow,建议将他们合并到一块儿操做,能够减小回流的次数。