性能优化涵盖的范围很是之广,其中包含的知识也很是繁杂。从加载性能到渲染性能、运行时性能,每一个点都有很是多能够学习与实践的知识。javascript
优化问题包含方方面面,优化手段也依场景和具体问题而定。所以,本文并非一个泛而全的概览文章,而是以以前的一次对于业务产品的简单优化(主要是DOMContentLoaded时间)为例,介绍了如何使用Chrome Dev Tools来分析问题,使用一些策略来缩短DOMContentLoaded的时间,提升加载速度。html
W3C将页面加载分为了许多阶段, DOMContentLoaded(如下简称DCL)相似的有一些 DOM readState ,它们都会标识页面的加载状态与所处的阶段。咱们接触最多的也就是 readState 中的 interactive、complete(或load事件)以及DCL事件html5
简单了解一下它们。浏览器会基于HTML内容来构建DOM,并基于CSS构建CSSOM。二者构建完成后,会合并为Render Tree。当DOM构建完毕后, document.readyState
状态会变为 interactive
。java
Render Tree构建完成就会进入到咱们很是熟悉的 Layout –>> Paint –>> Composite 管道。web
可是当页面包含Javascript时,这个过程会有些区别。chrome
根据HTML5 spec,因为在Javascript中能够访问DOM,所以当浏览器解析页面遇到Javascript后会阻塞 DOM 的解析;于此同时,为避免CSS与Javascript之间的竞态,CSSOM的构建会阻塞 Javascript 脚本的执行。不过有一个例外,若是将脚本设置为async,会有一个区别,DCL的触发不须要等待async的脚本被执行。浏览器
也就是:缓存
interactive
。即 "DOM tree is ready"。DOMContentLoaded
事件。即 "CSSOM is ready"。或者将上面的部分精简一下:性能优化
DOM construction can’t proceed until JavaScript is executed, and JavaScript can’t proceed until CSSOM is available. [1]bash
下面就能够经过Chrome Dev Tools来分析问题。为了内容精简,如下截图取了在slow 3G 无缓存模式下的访问状况,为了保持和线上环境相似(还原浏览器的同源最大请求并发),在本地搭建对应的服务器放置静态资源。wifi状况下,各个时间点大体等比缩短8~9倍。
首先看一个总体的waterfall
在最下面能够看到 DCL 为 17.00s(slow 3G)。
p.s. 页面load时间也很长。主要由于业务膨胀后,页面包含过多资源,没有使用一些懒加载与异步渲染技术,这部分也存在不少优化空间,但因为篇幅不在本文中讨论内。
页面里有一个很明显的请求block了DCL —— common.js。那么common.js是什么呢?它其实就是项目中一些通用脚本文件的打包合并。
因为common.js为同步脚本,所以等到它其下载并执行完毕后,才会触发DCL。而与此对应的,其余各个脚本的时间线与其有很大差距。具体来看common.js的Timing pharse,耗时11.44s,其中download花费 7.12s。
download过长最直接的缘由就是文件太大。common.js的打包合并包含了下面的内容
'pkg/common.js': [
'static/js/bridge.js', // 业务基础库
'static/js/zepto.min.js', // 第三方库
'static/js/zepto.touch.min.js', // 第三方库
'static/js/bluebird.core.min.js', // 第三方库
'static/js/link.interceptor.js', // 业务基础库
'static/js/global.js', // 业务基础库
'static/js/felog.js', // 业务基础库
'widget/utils/*.js' // 业务工具组件
]
复制代码
这里,咱们发现这么打包会存在下面几个问题:
download过长最直接的缘由:文件过大。
将这些资源所有打包在一块儿致使common.js较大,原文件161KB,gzip 以后为52.5KB,单点阻塞了关键渲染路径。你也能够在 audits 中的Critical Request Chains部分发现common.js是瓶颈。
zepto/bluebird这种第三方库属于很是稳定的资源,几乎不会改动。虽然代码量较多,可是经过HTTP Cache能够有效避免重复下载。同时,上线新版后,为了不一些文件走 HTTP Cache,咱们会给静态资源加上 md5。
然而,当这些稳定的第三方库与一些其余文件打包后,会由于该打包中某些文件的局部变更致使合并打包后的hash变化而缓存失效。
例如,其中bridge.js与/utils/*.js容易随着版本上线迭代,迭代后打包致使common的hash变化,HTTP Cache失效,zepto/bluebird等较大的资源虽然未更改,但因为打包在了一块儿,仍须要从新下载。每次上线新版本后,一些加载的性能数据表现都会显著降低,其中一部分缘由在于此。
结合上面分析的问题,能够进行一些简单而有效的优化。
考虑将文件的打包合并按照文件的更新频率进行划分。这样既能够有效缩减common.js的大小,也能够基于不一样类型的资源,更好利用HTTP Cache。
例如:
将基本不会变更的文件打包为 lib.js,主要为一些第三方库,这类文件几乎不会改动,很是稳定。
将项目依赖的最基础js打包为common.js,例如本文中的global.js、link.interceptor.js,项目中的全部部分都须要它们,同时也是项目特有的,相较上一部分的lib会有必定量的开发与改动,可是更新间隔可能会有几个版本。
将项目中变更较为频繁的工具库打包为util.js,理论上其中工具因为不做为基础运行的依赖,是能够异步加载的。这部分代码是三者之中变更最为频繁的。
'pkg/util.js': [
'widget/utils/*.js'
],
'pkg/common.js': [
'static/js/link.interceptor.js',
'static/js/global.js',
'static/js/felog.js'
],
'pkg/lib.js': [
'static/js/zepto.min.js',
'static/js/zepto.touch.min.js',
'static/js/bluebird.core.min.js'
]
复制代码
可是在拆分后DCL时间几乎没有减小。
这里就不得不提到打包的初衷之一:减小并发。咱们将common.js拆分为三个部分后,触碰到了同域TCP链接数限制,图中的这四个资源被chrome放入了队列(图中白色长条)。
Queueing. The browser queues requests when:
- There are higher priority requests.
- There are already six TCP (Chrome) connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only.
- The browser is briefly allocating space in the disk cache
咱们打包合并资源必定程度上也是为了减小TCP round trip,同时尽可能规避同域下的请求并发数量限制。所以在common.js拆分时,也要注意不宜分得过细,不然过犹不及,忘了初衷。
从network waterfall中也很容易发现,大部分资源因为size较小,其下载时间其实很是短,耗时主要是在TTFB(Time To First Byte),能够粗略理解为在等待服务器返回数据(图中表现出来就是绿色较多)。因此除了打包项目依赖的lib.js/common.js/util.js外,还能够考虑将部分依赖的组件脚本进行打包合并,
像上图中这四个脚本的耗时都在在TTFB上,并且在同一个CDN上,能够经过打包减少没必要要的并发。将首屏依赖的关键组件进行打包:
'pkg/util.js': [
'widget/utils/*.js'
],
'pkg/common.js': [
'static/js/bridge.js',
'static/js/link.interceptor.js',
'static/js/global.js',
'static/js/felog.js'
],
'pkg/lib.js': [
'static/js/zepto.min.js',
'static/js/zepto.touch.min.js',
'static/js/bluebird.core.min.js'
],
'pkg/homewgt.js': [
'widget/home/**.js',
'widget/player/*.js',
]
复制代码
优化后的DCL变为了11.20s。
注意,一些打包工具会自动分析文件依赖关系,文件打包后会同时替换资源路径。例如:在HTML中,引用了 static/js/zepto.min.js
和 static/js/bluebird.core.min.js
两个资源,在打包后构建工具会将HTML中的引用自动替换为 lib.js
。所以须要注意打包后的资源加载顺序。
例如,原HTML中的资源顺序
<script type="text/javascript" src="//your.cdn.com/static/js/bridge.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/zepto.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bluebird.core.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/global.js"></script>
复制代码
其中 global.js
依赖于 zepto.min.js
,这个在目前看来没有问题。可是因为打包合并,构建工具会自动替换脚本文件名。因为 bridge.js
的位置,在打包后common.js
的引入顺序先于lib.js
。这就致使 global.js
先于 zepto.min.js
引入与执行,出现错误。
对此,在不影响原有依赖的状况下,能够调整脚本顺序
<script type="text/javascript" src="//your.cdn.com/static/js/zepto.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bluebird.core.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bridge.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/global.js"></script>
复制代码
输出的结果以下:
最终在无缓存的slow 3G下DCL时间11.19s,相比最初的17.00s,下降34%。(wifi状况降低比例相同,时间大体同比为1/8~1/9,接近1s)。同时,相较于以前,一些静态资源可以更好地去利用HTTP Cache,节省带宽,下降每次新版上线后用户访问站点的静态资源下载量。
须要指出,性能优化也许有一些“基本准则”,但绝对没有银弹。不管是多么“基础与通用”的优化手段,亦或是多么“复杂而有针对性”的优化手段,都是在解决特定的具体问题。所以,解决性能问题每每都是从实际出发,经过“排查问题 --> 分析诊断 --> 实施优化 --> 验证效果”这样一条不断循环的路径来开展的。
同时,提高性能的其中一个目的就是更好的用户体验。用户体验每每是一个宽泛的概念,涉及方方面面。相对应的,性能优化也不能只死盯着某个“指标”,更应该理解其背后对产品与用户的意义。从问题出发,拿数据量化,找解决方案。
在实际环境下,面对有限的资源和各类限制,创造最大的价值。性能优化更是如此。