前端性能优化

这是一个老生常谈的话题,甚至有不少专家专门针对这个话题著书立说。(我没有骂人的意思)我希望这篇文章能让这个话题尽可能的通俗易通,不要那么高大上。

web 的场景越来越广泛,比如传统PC浏览器、移动端浏览器、客户端内嵌浏览器、移动端内置浏览器等等。 这样丰富的场景,也让web扮演的角色更加重要,她可能化身成一个移动端的APP。如果我们还只是停留在利用html\css\js把设计师提供的效果图还原出来,那是远远不够的。

1 优化的目标
—— 不是为了在各种测评软件中得个高分。(评测可以参考)

页面加载更快

缩短页面加载耗时。有一个旧逻辑‘8秒原则’,姑且拿来用用,大概的含义是用户有耐心等待页面加载完成的心理时间是8秒,超过这个期限,绝大多数用户就会放弃等待。

交互更流畅

页面上点击、拖动等交互行为,不能有明显的卡顿、延迟。

2 优化的方法
—— 优化是一个综合性较强的事情,往往包括前端优化、服务端优化、客户端优化等等。

前端优化

1 CSS sprite

将多张背景合并在一张,利用background-position来达到控制显示的作用。

css
background: url("imgs/icon.png") no-repeat;
background-position: -194px -132px;

用在背景图片上,这个很常见,其实图片合并成一张,也可以用在img标签上,不过需要利用 clip。(不常用)

css
position:absolute;
clip:rect(21px 68px 51px 38px);

2 小图以base64方式加载

html
src="data:image/png;base64,iVB...

如果你在windows下,对小图片转换成base64,感觉得不够方便,可以试试 ViewFileInfo ,关于这个工具,可以访问 viewfileinfo 了解更详细的信息。

3 CSS、JS 合并压缩

这部分工作既可以在前端处理,也可以在服务端处理。(html也可以压缩优化) 前端可以使用如下插件:

grunt-contrib-uglify、grunt-contrib-concat
如果你不喜欢nodejs,可以试试:yuicompress

如果是服务端,可以试试: load ngx_http_concat_module.so ,这是淘宝提供的扩展模块。

4 JS 延迟\按需加载

这个概念其实很好理解。不要一次性把所有的JS都加载进来,有一些JS可以放在页面首屏加载好了,再去加载。 也有一些可以放在用户交互后,根据需要再加载。

具体的方法可以参考:micro-loader

5 图片预加载

我们常用的有两种。

以css方式做预加载:


.preloader {
background-image: url(image1.jpg);
background-image: url(image2.jpg);
background-image: url(image3.jpg);
width: 0px;
height: 0px;
display: inline;
}

大部分浏览器都是只加载了最后一个图片,前两个图片被无视了。但是在webkit核心的浏览器中,比如chrome,会预加载这三个图片。

以js方式做图片预加载:

function preloader(arrImgSrc) {
for (var i = 0, nLen = arrImgSrc.length; i < nLen; i++) {
var preImg = new Image();
preImg.src = arrImgSrc[i];
}
}

如果真有需要,推荐用js方式来完成,控制起来更灵活。

这是一个思路,具体用什么方法已经没那么重要了。直白说就是想办法让浏览器加载这些图片,而不显示出来,当需要显示的时候,直接使用上。

6 图片延迟加载

这个跟图片预加载思路正好相反,由于浏览器的同源并发数限制,为了让页面的框架结构尽可能早点显示出来,我们可以将图片的地址写成一个默认的,当这块区域进入用户的可视范围,我们再将图片的src,从默认的换成真实地址。

关于lazyLoad不多解释,如果要了解更详细的信息,自行搜索。

7 非阻塞加载

有一种说法是css引入放在html-head里,js引入放在body后,主要是说js的加载会造成浏览器阻塞。

现代浏览器的进步,会让上述理论变为有待验证。

其实目前大家可以适当使用async、defer等属性来更灵活的控制是否异步加载。 如:

浏览器进步给前端开发带来了很多便捷。

8 享元模式

这个主要是控制页面拖动卡顿的。我们现在的信息流展示,常常会用到所谓的‘瀑布流’模式。 简单说一下:当用户拖动滚动条到用户可视区域边缘(经常是尾部),动态获取内容,并呈现给用户。

这里会有一个常见的问题,用户一直往下拖,加载的内容页越多,滚动条也变得越来越短,页面也变得越来越卡。

针对这个问题,常见的处理有两种:

连续加载多次后,以分页方式替换此前的交互模式。
享元模式
关于享元模式在此的体现:用户往下拖动,动态加载内容的同时,页面头部卷入非可视区域部分内容,从dom中剔除,使得所有的数据共享同一块数据展示区域。

当然这里需要有一些细节处理,比如剔除的内容缓存起来,当用户往上回滚的时候,剔除的内容加载进来,而尾部的内容又该剔除出去。

这个在移动前端开发中尤为常见,因为移动端webview的性能相对PC而言,还是有不少坑。

9 避免重复查找

这是一个典型的空间换时间。比如我们经常要操作一个dom, $(‘#list’);如果我们每次都调用$函数,就造成了多次重复查找遍历等。

var $list = $(‘#list’); // 用变量缓存函数结果,明显就能提升性能。

这个思路,也同样可以用在编程的方方面面。比如我们常用的for循环:

js
for(var i = 0; i < items.length; i++){...}

items.length 就该用变量缓存,避免每次都进行属性查找。

举一反三,比如在你的函数里多次util.cookie如此调用, 不如用var cookie = util.cookie,先缓存下,节约每次属性查找的消耗。

10 减少重绘

先看个案例:

js
<a href="javascript:;" id="example">I'm an Example</a>
var example = document .getElementById("example");
example.ondblclick = function() {
example.style.backgroundColor = "red";
example.style.width = "200px";
example.style.color = "white";
}

首先把背景色改为红色(第一次重绘)
然后把宽度改为200像素(第二次重绘)
最后把前景色改成白色(第三次重绘)

你当然可以把这三次操作合并成一次,但使用css,貌似更优雅:

js
example.ondblclick = function() { example.className = "dblClick"; }

本文多数方法都以案例在讲解,不过我希望是不要局限在这些案例里,比如就减少重绘再举一个不重绘的场景。

我们经常使用tab switch形式菜单,当光标在其上的时候,触发对应的事件,界面也在发生变化(重绘),甚至会去发起数据请求。但实际上有时候用户是快速划过,并非有意要停留在具体的菜单上。

这里我们应该降低事件的响应灵敏度。比如设置一个200毫秒的时间,只有当光标在这个停留达到200毫秒才去处理对应的事件,否则不要变化。

你会发现第二个范例跟第一个范例,差异很大。所以我希望传递的是一种思路,而不是具体的一个场景方法。

11 application cache

这是属于 html5 的新特性,如果使用的webview中,请确保客户端是否启用相关设置。

由于这个特性还有一些潜在问题,这里不详解。

有兴趣的同学请移步我的另一个github项目appcache

12 Local Storage

如果使用的webview中,请确保客户端是否启用相关设置。 这个特性使用好了,能带来特别多的好处:

替换一些跟服务器没有任何关系的cookie使用场景,有效减小通信体积。记录上次的一些表单,避免每次用户都要填写,给用户带去方便。

另外还可以用来缓存页面资源,用来做本地化。

13 预加载

在HTML5中,有个很有用但常被忽略的特性,就是预先加载(prefetch),它的原理是: 利用浏览器的空闲时间去先下载用户指定需要的内容,然后缓存起来,这样用户下次加载时,就直接从缓存中取出来,效率就快了.

目前,只有firefox和chrome支持这两个特性,chrome是在version 13后开始支持的,safri和ie依然不支持.

举个例子说明:比如要预先加载某个页面,可以这样:

html
<link rel="prefetch" href="http://www.example.com/"> <!-- Firefox -->

但如果是google的话,要用另外的一个名称,即:

html
<link rel="prerender" href="http://www.example.com/"><!-- Chrome -->

即使在不支持的浏览器,用了这个特性其实是不会出错的,只不过浏览器解析不到而已.

14 DNS预解析

要知道DNS的的解析成本很高滴,往往导致了网站加载速度慢。现在浏览器针对这个问题开发了更智能的处理方式,它将域名缓存后,当用户点击其它页面地址后自动的获取。

如果你希望预先获取DNS,你可以控制你的浏览器来解析域名,例如:

html
<link rel="dns-prefetch" href="//www.example.com">

补充一下,这个是HTML5新增的。

15 其他

避免js中使用with, eval等可能影响性能的操作;
避免CSS使用js表达式;
避免同一个资源,url不同,比如: http://www.example.com/a.gif?v=2011 与 http://www.example.com/a.gif?v=2012
服务端优化

1 缓存设置优化
资源文件,如果不设置缓存,相当于每次浏览器都要到服务器来取,这是多么痛的开销。


location ~ .*.(gif|jpg|jpeg|png|bmp|swf)$ {
expires 30d;
access_log off;
}

上面是常见的nginx中的关于缓存的一些配置, 如果用nodejs来表达:


res.setHeader("Last-Modified", lastModified);
res.setHeader("Expires", expires.toUTCString());
res.setHeader("Cache-Control", "max-age=" + maxAge);

主要控制的是http协议头如下几个字段: “Last-Modified、Expires、Cache-Control”。

能大大减轻服务器压力,也能很大程度提高客户端非首次访问的速度。

2 gzip压缩
对于文本资源,启用gzip压缩,能大大减小通信数据的体积。

如果你对本文提到的一些nginx配置有兴趣了解更多,可以移步到 myblog。

3 CDN加速
采用CDN 能有效的提高性能。

4 其他

1 增加域名

把网站资源部署到不同的域名下,以此来针对浏览器同源并发限制的策略作优化。 需要注意的是,要考虑下DNS解析带来的消耗。如果你网页本来并发资源就少,那没必要。

2 不同的服务器采用不同的策略
比如专门启用 imgs.xxx.com来专门部署图片资源,那我们就可以针对图片服务器有不同的策略,比如不同的文件系统(TFS)。对于这样的服务器,硬件上应加强I/O性能,可以适当减低CPU性能。

还有一个点,稍微提一下:由于不同域,通信中,能避免不必要的cookie往返,减小通信的体积。

3 适当启用缓存
这里的缓存跟前面的缓存,侧重稍微不同。

redis,memcache等,一个数据从内存中获取,往往会比从磁盘获取更快,对吧?
一段源代码,跟opcode相比,明显后者会快许多。(phper 不应忽视这个概念)。
4 模板
我建议首屏内容采用服务端模板引擎生成,能减少一些http请求,非首屏再用ajax请求。当然如果每个用户看到的首屏内容都不同,那不启用也没关系。

这么好的场景,不打个广告有点对不住自己。一个适用于前后端的模板引擎:template4js

5 Big Pipe
这个概念,首先被facebook发扬光大。

原理介绍,利用分块传输(Transfer-Encoding: chunked)让浏览器也尽快并行解析。

如果用nodejs实现,那真的很简单,那么先在后台将数据分成几个模块:

首先还是设置http头,然后分块传输,response.write(chunk),当所有的块传输完毕,最后response.end()结束传输。

这个优化主要是减少http请求,对于服务器端来说这个意义可能更大一些。

6 监控
把问题暴露出来,针对性的处理问题。 比如性能优化要杜绝404,这个其实也可以放在前端层面处理,比如ajax请求接口的时候,可以把性能统计出来,如果娴这个数据量太大,也可以简单点,只统计超时、出错的接口。因为这个统计,会比观察服务器日志更全面一些,起码他包括了部分请求未走到服务器的情况。

客户端及其他优化

预加载页面
对于这个用得不多,但在一些内嵌网页的客户端中往往有特别的效果。 简单说先启用浏览器控件并把网页加载好(但隐藏),当用户点击到入口的时候,瞬间显示出来,好流弊,不过如果用户不点击,对服务器来说就是徒增压力。

启用该启用的设置
如果是移动APP,尽量把local storage等都启用了,别在前端需要的时候发现各种坑。

本地化
也可以配合前端页面做本地化,拦截已经缓存的请求,如果本地有则从本地返回。

客户端开发经验少,所以这个点上,我提不出多少内容。我打算接下来抽时间了解了解android+webview的开发,争取把这个环节再丰富下。希望有人能期待我的下一次总结。

3 总结
通过此文,你不难发现,有时候我们在延迟加载、有时候我们在预加载;有时候我们在降低事件灵敏度,有时候我们又在拼命想办法提高事件响应的灵敏度(此前移动端zepto tap事件会有300毫秒延迟)…… 我只能说万变不离其宗。

重构是一种态度,优化一种精神。重构、优化,让我们不断完善自己。

有话要说