鸟瞰前端 , 再论性能优化

前端工程师是一个最近这5-6年才开始慢慢被互联网公司重视起来的一个职业,可以说是一个新兴行业,我用一张简单的思维导图带大家回顾一下前端技术发展的历程以及未来一个展望:

bigsec

1.0时代没什么说的,html、css打天下的时代,那个时候你会用js开发个计算器就牛逼到不行。2.0时代是最好的时代,新技术、新思想蓬勃发展,堪称前端的工业革命,前端人员的地位得到了充分认可,门槛也有一定的提升。前端性能优化的涉及点从服务器到协议再到宿主环境本身都要有比较深刻的认识,业界目前主要还是以雅虎总结出来35条前端性能优化的黄金军规(http://www.cnblogs.com/siqi/p/3655436.html) 为参考。今天我想将这些年对前端的性能优化的经验思考整体来个串烧,带大家鸟瞰一下前端性能优化目前的一些通行做法以及这么做的出发点。文章初衷主要是对一些性能优化基础知识回顾和体系梳理,不对具体技术点做深入分析,点到为止,个人理解不对的地方欢迎各位大神拍砖,抛砖引玉。

引入话题前我还是先从一个老生常谈的话题开始:

“从用户输入URl到页面展示给用户浏览器客户端的过程中发生了什么?”

这里用个图表简单描述一下几个步骤:

bigsec

web优化的目标就是如何让用户更快、更简单易用、更流畅的使用我们的服务,对于前端开发而言就是如何让我们的资源体量更小、数量更精简、内容更早呈现、交互更加人性化。

web性能优化有个大家比较公认的二八原则,就是资源从服务器处理完下发到客户端的浏览器上(上图第6步)所占的时间比例大概是整个过程的20%,也就是说服务器端可以优化的空间的效率提升并不会很明显,前端性能优化成为web性能优化重点考虑的领域,我下面将会从以下几个维度去做了自己的一个思考(跟35条军规有一定重叠)和总结:

bigsec

一、浏览器宿主环境

1、突破单线程解析渲染阻塞限制

浏览器是一个单线程解析模式去解析渲染从服务器端拿到的html文本,css加载的过程中会对后续的脚本资源加载造成阻塞,脚本的加载也会阻塞后续DOM结构的解析造成页面的留白时间增长,雅虎的35条军规中有一条就是样式文件放在头部,脚本文件放在DOM节点最末尾,减少阻塞。这里还有几个针对脚本文件的优化:

  • 针对不需要DOM操作(主要考虑是需要操作DOM的脚本往往需要获取一些样式信息)的Js脚本可以采用动态创建script的方式载入,动态载入的脚本不阻塞后续资源的加载。
  • 脚本文件加载可以加上defer或者async属性标识防止阻塞(关于两者区别可以参考)

2、利用事件冒泡特性

浏览器的事件模型的冒泡的特性(浏览器事件模型不清楚的自行搜索了解)我觉得是最牛逼的设计之一,解决了浏览器因为解析DOM模型不同步导致开发者往DOM对象注册事件回调找不到对象的问题。

浏览器事件注册有3个级别定义,DOM 0级事件注册(利用DOM元素行内事件属性onclick注册事件回调),DOM 1级事件注册(利用DOM元素对象的onclick API 在外部注册事件回调),DOM 2级事件注册(利用利用DOM元素对象的addEventListner/attachEvent API 在外部注册事件回调)。这里性能优化的建议就是利用DOM2级在目标DOM的父标签(大部分框架是在body标签统一注册事件监听)注册回调,收拢事件监听入口同时节约了DOM节点引用开销。

3、避开Cookie性能bug

Cookie是前端作为前后台登录态校验最通常用的缓存方案,但鉴于浏览器在每次都会往同域的任何资源的http请求中自动带上cookie信息的情况,这里有必要进行优化一下,因为像css、js、image这些资源请求是不需要cookie信息的,会无端造成请求带宽的浪费(想象一下我们的cookie大小假设为10K,100个请求就是近1M的大小,高并发下以我们现行网络带宽也是蛮大的一笔负担了)。Cookie free性能优化方案的处理方式是CDN异域静态资源服务器部署我们的前端css、js、image资源。

以自己目前负责的香港跨境汇款为例

bigsec

页面路径下的资源的请求:

bigsec

CDN资源加载的请求:

bigsec

通过对比CDN分开部署的资源请求并没有带上cookie信息。

4、突破浏览器并发连接限制

浏览器针对domain,而非页面page做并发连接限制的特性,domain hash的技术优化方案的处理方式是将资源划分域分开部署,但因为过多的域划分会增加多余的DNS开销,这里通行的数量是3个以内。目前我们的港菲汇款业务只有两个域名分开部署,一个主站,一个CDN,我个人建议可以将CDN中的图片资源再单独再分一个域名部署会更好些,为什么单独把图片抽出来,后面会讲到。

5、利用GPU硬件加速浏览器渲染

针对一些界面渲染过程比较耗时的情况下,可以利用CSS3属性开启GPU来加速渲染我们的DOM,开启很简单一般我是用-webkit-transform:translateZ(0)假3D属性来唤起系统GPU加速渲染功能,关于为什么会这样,我这里做个简单的解释:

对于我们的浏览器而言,拿到我们的html文本串开始按顺序解析成DOM树,并与同步解析出来的CSS匹配生成渲染树(跟DOM树的节点不是一一对应,比如display:none的节点就不会插入渲染树)

bigsec

图片来源 https://segmentfault.com/a/1190000008650975

浏览器将渲染树的节点用一个图层表示,这样层层叠加在一起生成layout,有点像ps的图层叠加的概念(可以通过火狐浏览器开发者工具3维展示更直观),一般情况下对节点的任何涉及尺寸的改变都会引起layout的重排重绘(重排和重绘是造成浏览器渲染的最大性能损耗的因素),但有种开小灶的情况Composite Layers(复合图层)直接交给我们GPU中单独的合成器进程处理,自身变化不会引起其他层的位置变化,不会引起重排重绘。tranform 3d属性是可以悄悄的告诉我们的浏览器把元素解析作为复合图层交给单独进程去处理的。

注:这里有个原则,不能滥用我们的加速,因为过多开启硬件加速会消耗更多的用户内存空间,也会比较耗电,一般针对css3动画建议开启

二、Http维度

1、减少http请求数量

a、通行解决方案

  • css、script合并:gulp、webpack都能够很简单的通过任务脚本的方式去自动化解决,目前我们团队是用我们自研的前端构建工具配合我们的Dust库做的发布前的资源打包任务,核心就是用的gulp。
  • css sprites雪碧图:将网站常用的一些小图片整合到一张大图上来,样式里面通过background-position二维坐标定位找到自己的图片。这里有个原则,一般是将网站复用率较高的,不太容易变动的图标和图片,比如按钮、平铺背景小图片等。
  • font-icon字体图标:字体图标库的使用,是一个非常有创新的方式,因为是矢量的,解决位图像素放大变虚的问题,体验很好,相比同样矢量的SVG来说使用更简单,一个css的font-family就可以像平时设置字体一样使用,淘宝是国内这方面的先行者,有自己的一套很开放的矢量图标库平台。淘宝自身的许多小图标都是用的字体图标来展示的。

bigsec

bigsec

图片base64编码传输:图片base64编码后,可以让浏览器减少自身的一次http请求,但因为自身的一些缺陷,不能滥用(即使一个很小的图片编码后都会有一大串字符,增加了我们CSS体积,性能不降反升),我的建议是针对那些全站通用或者体积很小不好整合到雪碧图里面的图标进行编码,当然还有很多不同的场景大家自己权衡。

图片延时加载:主要是为了减少首屏一次性图片的加载量。具体做法是给图片或者标签设置一个私有行内属性data-image(当然可以自己随便定义)存放目标图片地址信息,监听浏览器的滚动事件,标签到了浏览器可视区域就将图片地址放入图片的src属性中或者作为标签的样式的背景图片中展示。淘宝首页的做法是用一个div来做延时图片加载,通过背景图片来展示最后的图片。

图片展示前:

bigsec

图片展示后:

bigsec

b、缓存机制

协议缓存方案:利用http缓存协议头cache-control做304缓存,或者更精确的ETAG设置依据资源的修改时间来设置缓存方案。但目前更有效或者极端的做法是利用max-expire-time,设置资源的最大缓存时间假设为1年的长缓存,更新采用非覆盖式更新的方式是目前大公司通行的做法。这样每次资源请求的时候都是只从客户端缓存读取(status:200,size:from cache),而不是还要跑一次http请求到服务器端拿到304状态。还是以一张淘宝首页图片长缓存的截图为例:

bigsec

appCache应用缓存方案:离线应用缓存是h5提供一个比较有效的离线应用方案,利用navigator.online 、window.applicationCache对象、服务器.appcache(以前是.manifest)配置文件保证在脱机下的移动web应用照常能用,如果要做数据的离线还要加上window.localStorage做离线数据的保存。这里简单说一下接入离线应用需要的几个步骤: 1、给需要做离线缓存的页面html标签设定manifest属性,指定缓存的配置文件 cache.appcahe(可以设定任何扩展名,只要在服务器端配置mime-type为text/cache-manifest就行)。

2、创建上一步指定的cache.appcache配置文件,按以下截图说明来配置资源 bigsec

3、在服务器端配置配置文件的扩展名映射的mime-type为text/cache-manifest

bigsec

appcache离线方案诟病太多,目前接入的不多,有种慢慢变弃儿的趋势,这里提出来让大家权衡

PWA(Progressive Web Apps)方案:谷歌提出的一套全新的离线web方案,利用manifest.json配置文件、window.serviceWorker对象来实现类原生app体验的离线应用方案,可以说是浏览器应用缓存的一个脱胎升级方案(鉴于文章篇幅,这里不做介绍了)。

2、减轻http数据请求大小

a、通行解决方案

css、script、图片压缩:这些可以gulp或者webpack自动化脚本里面定义脚本任务来完成。

服务器开启gzip压缩:一般现在服务器都有开启Gzip压缩,压缩率通常都是30%以上,效果还是不错的。

原图:

bigsec

Gzip压缩后:

bigsec

图片服务器动态响应方案:这个方案对应上面宿主环境维度domain hash单独出来一个独立域名部署图片资源的方案介绍。图片资源是网站请求资源中一个非常大头的开销,以前大家可以在静态资源服务器中建个image目录存放就完事,随着网站服务发展,图片不仅面临多样化、高并发带来的压力,在移动端wap站点中更是要针对不同的分辨率屏幕下图片尺寸动态适配的场景以为了节省带宽的需求。图片服务器的单独架构有一定的复杂度(如果考虑到高并发下的容灾、缓存机制的话不亚于一个大型web网站集群的搭建,这里有篇文章推荐大家阅读),这里只讨论一下其中负责切图服务部分的服务器(简称切图服务器)功能,切图服务器对外提供一个restful的url调用,比如http://www.xxx.com/xxx图片路径…/xxx图片名/130_120 告诉切图服务器将“xxx图片路径”下的xxx图片等比压缩成130*120的图片尺寸并返回,这块服务可以使用我们前端比较熟悉的node创建,当然也可以用PHP来提供。

b、页面切片预加载方案

性能优化静态资源维度最后一块内容就是针对页面,如何尽早输出页面模块,减少留白时间是一个思考点。facebook应用的BigPipe方案是个很不错的借鉴思想,还有淘宝也有首页做了相应的切片方案,对页面合理的分块,在服务器和客户端建立某种对应机制,让各个页面块并行的在服务器端拼接完成并吐出来,目前我对这块没有太深的了解,这里只是提出bigPipe的方案供大家参考。

三、TCP维度

TCP连接中的3次握手、慢启动的一些特性注定了连接通道的利用效率成为制约性能的一个很大的因素。因为http是基于TCP的应用协议,TCP层维度考虑还得从http几个版本的发展历史来看:

  • http1.0时期:tcp连接是基于一种单通道顺序等待请求响应方式(客户端每发一个请求都要重新建立连接),特定历史背景下产生的,低效率很难跟上时代发展,99年在1.0基础上修订出1.1版本,并沿用至今。
  • http1.1时期:在请求头信息加入keep Alive保持连接的一定活性(当然也加入了100 Status节约带宽、cache特性等),允许在一个连接通道生命期内重复发送不同的应用请求,一定程度上减轻了连接资源利用效率问题,但当用户浏览网页时间大于连接活性周期再次请求的时候仍然要重新建立这些请求,在大型科技公司对高并发高可用性下资源高效利用的背景下,1.1版本还是难满足大公司对高性能条件下网络资源的高效率利用的要求。

谷歌(叒是谷歌,牛逼)率先在09年基于TCP开发出全新SPDY应用协议,解决了多路复用请求优化、服务器推送的痛点问题,也为后面http2.0的推出奠定了基础。 我们可以做的优化:减少一些不必要的请求(扫除404死连接、304请求用我们的长缓存机制)去优化,尽量减少一些不必要的连接请求数。

四、代码实现

鉴于js语言本身的灵活性,以及每个人的开发习惯,很难有很好的一个方式去验证开发者的代码实现的效率(目前更多的是用打点测速的方式去监控代码的执行时间),更多的是一种建议,大家有更好的建议可以提出来分享。

  • 单线程限制:利用异步回调&多线程API突破js语言单线程带来的内存开销利用不充分的问题,现有可以利用的一些异步方式的回调都可以尝试利用比如settimeout,setinterval,requestAnimationframe(推荐用它),多线程的API方式有WebWork。这里推荐日本的一个开发者开发的一个多线程前端库Concurrent.Thread.js(它是作者用setTimeout和setInterval来模拟的多线程,可以自行网上搜索了解)。

  • 重点优化代码中的循环结构体:就代码本身而言影响执行效率的大部分是循环体结构和算法设计不合理导致的时间复杂度和空间复杂度的增加。这里放两段实际项目中的代码截图来对比:

实例功能需要:实现输入框每4个字符一个空格隔开的效果

低效实现方式,用for循环:

bigsec

改进后的方式:

bigsec

合理使用设计模式优化代码结构:设计模式的合理利用(不能滥用)可以起到内存优化提高执行效率效果,比如单例和简单工厂在创建xhr请求对象中的利用:创建一个简单工厂向外面提供xhr对象,工厂内部用闭包开辟一个数组队列单例用来存放xhr对象,当调用者需要xhr对象时工厂就从队列里取出readyState状态为空闲(0/4)的xhr对象否则重新创建并放入队列中。在有大量ajax对象请求的应用下可以最大限度节约创建xhr消耗的内存开销,这里用个简图来描述一下思路:

bigsec

其他一些优化建议:比如减少js频繁操作dom节点的次数(这个本来想放在宿主环境维度)减少浏览器的重排重绘;比如针对dom标签对象(主要是针对ie下dom对象的引用会被GC回收遗漏的问题)、闭包内部的引用类型的变量用完过后记得要及时释放,避免造成内存泄漏。 五、产品交互逻辑 性能优化一般都是从技术角度去入手,但我们的目标之一“让用户简单易用”也是性能优化的一环。当技术性能缺陷难于避免的时候,作为前端交互实现的执行者,更应该配合产品和交互设计师提出一个我们认为更好的交互逻辑体验方案去让我们的数据加载不那么让用户有等待的感知,让我们的提示更加的人性舒服。(交互设计师更加专业,我这里不敢班门弄斧)

Web3.0时代的前端展望: 文章最后对Web3.0时代做个自己的猜想,web3.0目前在业内还没有很明确定义,大家可以大胆猜想前端行业未来形态。我在这先YY一下,人工智能、大数据广泛应用应该会成为推动前端进入3.0的时代的最好契机,以此引发的前端新的革命:

  • 浏览器成为一个系统生态(至于哪个浏览器现在不好说,现在谷歌浏览器PWA方案提供给前端类app开发方案就有这个趋势,以后都不需要装系统了)。前端不再是数据的搬运工,在web领域很有可能除了底层维护数据库的工程师们继续深耕外(主要切入云计算领域),其它的都可以转到前端做浏览器系统生态(哎妈呀,有种翻身农奴做主人的感觉O(∩_∩)O~~)。 传统的html语义性的超文本标记语言已经很难承载更多的信息化数据,html5继续深度发展,不再只是浏览器专用的标记语言,可能会成为跨平台标准的信息表示层,信息表示多样化前所未有,可能到时候不叫html了。
  • http2.0成为一个广泛试用的标准,并进一步深化,安全校验层做到类似区块链去中心化的思路,做到极致安全(https估计可以下岗了)。
  • js成为跨平台公认的标准实现语言(目前前端跨平台基础形态已经有了),随着Es6的广泛推广和深度改进,可能就不会像现在这么灵活,更加像一个合格的标准“高级”脚本语言。 (以上猜想纯属个人理解,没有权威认证,大家可以畅所欲言)

作者:刘勇刚 来源:http://www.cnblogs.com/qcloud1001/p/7561498.html