0.1秒的价值!浅谈Web前端页面提速问题
记得面试现在这份工作的时候,一位领导语重心长地谈道——当今的世界是互联网的世界,IT企业之间的竞争是很激烈的,如果一个网页的加载和显示速度,相比别人的站点页面有那么0.1秒的提升,那也是很大的一个成就。
然后我不知道怎么写下去了,就在群里问了那群狗头军师,结果是这样的。。。
好的,是时候“语锋一转”切回主题了 —— 如何提升我们站点页面的访问速度、减少等待时间,从而最大化地提升用户访问体验呢?
针对这个问题,我们今天会从前端的角度来提出系列解决方案,它们都能有效地提升你页面的访问速度。
一. 减少对服务器的文件请求
常规的HTTP请求属于“请求”-“应答”-“断开”形式的短连接,每一个独立的资源我们都会向服务器发去一份get请求,再等服务端将我们需要的文件传回来。每一次资源的请求都实实在在地耗费了一次“连接-等待-接收”的时间(当然将http请求设为keep-alive长连接状态可以减少“连接”的次数和时间),如果我们能有效减少对服务器文件的请求次数,便意味着我们可以从这块省下一些页面等待时间,也可以顺便减少服务器的负担。
对于这个解决方案,我们可以这么做:
1. 使用css sprite技术合并多个图片为单个图片文件,实际使用时通过background-position来定位背景位置(相信大家第一个想到的也是这个吧);
2. 合并多个css样式文件为单个样式文件,合并多个脚本为单个脚本,再在页面中引用合并后的样式/脚本文件。对于这个你可以使用r.js来帮忙,但我个人倒是不怎么推荐这个方法,因为合并了文件之后,多个页面之间公共部分的样式/脚本文件就无法缓存到客户端了;
3. 使用base64编码来展示图片。就如图github 404页面那样:
你可以使用这个工具来帮你把图片转换为base64编码的文件流,但常规只推荐你把这种方式使用在用户重复访问量较少的页面,因为它们虽然无须从服务端get一遍,但也无法缓存在客户端,导致用户每次访问页面都要重新渲染一次。而且冗长的文件流代码会占用你页面很大的代码空间,维护起页面来估计也会挺心塞;
4. 将小块的css、js代码段直接写在页面上,而非在页面引入独立的样式/脚本文件。相信有的朋友看惯了“保持结构 (标记)、表现 (样式)、行为 (脚本)三者分离”的规范,对此观点可能有些意见。只能说规范不是教条,适合自己的才是硬道理。直接把小段的、复用率低的样式/脚本直接写于页面上带来的利还是大于弊的(弊可能也就是增大了页面代码量、不那么好维护了点)。反观所有主流门户网站的页面源文件,基本没有一个是把样式/脚本都全部作为外部文件引入的(无论他们是否从减少服务器请求这点出发,事实都是这样);
5. 利用http-equiv="expires"元标签,设定一个未来的某时间点作为页面文件过期时间,用户在过期时间之前所获取到的页面文件都仅从缓存中去取。不过这个办法太死板(有时候即使服务端及时把过期时间更改为已结束时间,客户端可能都不会按照新更改的规则去服务端获取新文件资源),常规是不推荐使用的。
二. 减少文件大小
文件太大(特别是图片)导致加载时间较长,往往都是影响页面加载体验的头号大敌,那么尽可能减少请求文件的大小便是相当重要的事情了,我们可以做的事情有:
1. 压缩样式/脚本文件,就此你可以使用gulp或者grunt来实现这点,它们均能很好地减少css/js文件的大小(对于js还能起到混淆变量、函数名的作用);
2. 针对性选择图片格式,在无透明背景需求下,对于颜色较单一、无色彩渐变的图片仅使用gif格式,对于jpg图片也可按照其清晰度要求,在导出jpg的时候选择对应的“品质”进行优化:
如果你喜欢尝鲜,可以学淘宝那样使用webp图片格式,它能很好地优化同画质下的文件大小:
3. 使用Font Awesome来替代页面上的图标,其原理是使用@font-face让用户下载一个非常小的UI字体包,把页面上用到的图标以字符的形式来显示,从而减少了图片需求和图标文件大小。
三. 适度使用CDN
使用CDN有几个好处:如果用户在其它站点下载过这个CDN资源,那么来我们站点仅仅从缓存获取即可;减少了对自己站点服务器的文件请求(外部CDN的情况下),减少服务器负担;多个域会使浏览器允许异步下载资源的最大数量增多,比如一个站点只从一个域来请求资源,那么FireFox只允许同时刻最多异步下载2个文件,但如果使用了外部CDN来引入资源,那么FF允许在同时异步下载本域中的两个资源外,还额外允许同时异步下载另一个域(CDN)下的2个资源。
但是使用CDN有一个很大的问题——增加了dns解析的开销,如果一个页面同时引入了多个CDN的资源,可能会因为dns解析而陷入较多的等待时间,导致得不偿失。
对于这个问题,常规是建议一个站点下只使用同一个可靠、快速的CDN来引入各种所需资源即可,也就是说,建议一个页面从2个不同的域(比如站点域和CDN域)下来请求资源是最佳的选择(据说这个结论是雅虎前端工程师提出的)。
四. 延迟请求、异步加载脚本
在各主流浏览器下,常规情况,我们的脚本文件跟随其它资源文件一样都是异步下载的,但这里存在一个问题——比如FireFox下载好脚本后的一小段时间内会有“执行阻塞”的情况发生,也就是说浏览器下载好脚本后执行它的这段时间里,浏览器的其它行为被阻塞,导致页面上的其它资源都是无法被请求和下载的:
如果你页面里存在js代码执行时间过长的情况,那么用户就会明显感觉到页面的延迟。解决这个问题有一个简单的方法——将脚本请求标签放置到</body>结束标签前,使得页面上的脚本成为最后被请求的资源,自然也不会阻塞其它页面资源的请求事件了。
另外,虽然上面提到“我们的脚本文件跟随其它资源文件一样都是异步下载的”,但异步下载不代表异步执行,为了严格保证脚本逻辑顺序和依赖关系的正确性,浏览器会按照脚本被请求的先后顺序来执行脚本。那么问题就来了——如果页面上的脚本依赖关系并不大,甚至没有任何相互间的依赖,那么浏览器的这套规则就仅仅增加了页面请求阻塞时间而已(就像你花大钱买了一笔保险,但被保险期间你平安无事啥都没发生…… 嗯,这个比喻有点反人类……)。
解决这个问题的办法无非就是让脚本无阻塞地异步执行,比如给script标签加上defer和async属性或者动态注入脚本(可以参考这里),但这些都不是良好的解决方案,要么存在兼容性问题,要么太麻烦还无法处理依赖。
个人是推荐使用 requireJS(AMD规范) 或 seaJS(CMD规范) 来异步加载脚本并处理模块依赖的,前者将“依赖前置”(预加载所有被依赖脚本模块,执行速度最快),后者走的“依赖就近”(懒加载被依赖脚本模块,请求脚本更科学),你可以根据项目具体需求来选择最合适的。
五. 延迟请求首屏外的文件
先解释下,“首屏”指的是页面初始化时候的页面内容显示区域,也就是页面一加载,用户就首先看到的区域。
比如像京东啊淘宝啊,对于需要滚动页面才能看到的图片内容,都做了类似lazyload的处理,这些无非都是走了代理模式的理念,但的确给用户一个错觉——这个页面更快地加载完了,因为我很快就看到了屏幕上的内容(即使我还没下拉滚动条,而页面后方的文件其实还没真正加载呢)。
我们可以这样实现此方案,不依赖任何lazyload库,拿图片来做示范,我们可以这样编写首屏外的图片(假设某张图片地址是a.jpg)的img标签:
<img src="data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="a.jpg">
如上所示,页面初步加载这张图片的时候是直接以base64的方式(当然你也可以统一使用一张占位图loading.gif来替代)来快速显示一张极小的图片的,而图片本身的真实路径是存在data-src属性内的,我们可以在页面加载结束后再向服务器请求它真实的文件并替换:
function init(){
var imgDefer = document.getElementsByTagName('img');
for (var i=0; i<imgDefer.length; i++) {
if(imgDefer[i].getAttribute('data-src')) {
imgDefer[i].setAttribute('src',imgDefer[i].getAttribute('data-src'));
}
}
}
window.onload = init;
如上是对图片的延迟加载处理,对于视频、音频文件,可以采取完全一样的原理来延迟加载,从而有效减少页面初始化等待时间。
六. 优化页面模块排放顺序
这里有一个很好的例子,比如有一个页面是这样的——左边是侧边栏,用于存放用户的头像啊、资料啊,以及网站投放的广告啊,而右侧是文章内容区域:
那么我们的代码很可能是这样的:
<body>
<sidebar>
<!-- 侧边栏内容 --></sidebar>
<content>
<!-- 文章内容 -->
</content>
</body>
于是乎,浏览器按照它的UI单线程准则从上到下先加载了侧边栏,再加载我们的文章。。。
很明显,这样不是一个人性化的加载顺序,我们得弄清楚,页面上各个区域模块,对于用户而言,哪个才是最重要、最应当首先展示的。
对于上面的例子,文章内容才应该是用户首先要看到、需要浏览器优先请求和显示的区域。所以我们得修改我们的代码为:
<body>
<content>
<!-- 文章内容 -->
</content>
<sidebar>
<!-- 侧边栏内容 -->
</sidebar>
</body>
当然这里仅仅是用一个小示例来挑起各位的脑洞,懂得举一反三和实际运用才是硬道理。
七. 其它建议
1. 不要在css中使用@import,它会让一个样式文件去等待另一个样式文件的请求,无形中增加了页面等待时间(当然如果走的scss,@import就是另一回事了,呃跑题了~);
2. 避免页面或者页面文件重定向查找,这相当于你走进了一间卫生间,然后看到上面的牌子说“此处不同,请去前面左拐的卫生间”,又得重走一遍;
3. 减少无效请求——比如通过css/js来请求一个不存在的资源,可能会导致较长的等待和阻塞(直到它返回错误信息);
4. 无论你是否决定将脚本放到页尾,但一定要保障脚本放置于样式文件后方;
5. 配置.htaccess文件、走Gzip页面压缩形式、开启keep-alive连接模式等后端解决方案,这边就不细说了。
相信页面提速的方案绝对不仅仅局限于上面提到的这些,而且每个项目都有着各种需求和情况,只能按需选择适合自己的方案。
但最重要的还是——尽量把用户的体验放在第一位,无论是页面的加载或者交互,都应当多从用户角度切入去思考和设计最优选的方案,这样相信你一定能做出出色、受欢迎的作品。
今天聊的就是这些,共勉~
下一篇:HTTP协议中状态码的含义