前言
不知不觉离开京东已经快半年了。回想起这4年,很多的是感恩。
复盘了很多这些年自己学到的东西,查阅了很多的资料,对前端性能优化做一个总结。作为一个前端开发者,性能是我们关注的指标。它直接影响着我们的用户,同时也影响着产品本身。前端发展以来,优化方式,琳琅满目,这些内容复杂繁多,往往容易被人遗忘。本篇整理了很多东西,希望对大家有帮助。
正文
前端优化层出不穷,移动端大行其道的现在,我们可以说优化好移动端,PC端也将会更好。所以,我们可以综合以下图片进行一些分析,如图:
图中已经对前端性能做了一些概括。但其实,我觉得我们可以将这个概括更加精准,扼要,丰富。所以,接下来我会从四个方面就前端性能进行总结:首屏优化,网络方面、DOM操作及渲染方面、数据方面。
首屏优化
这两天一直在看首屏优化的文章,所以将其总结归纳一下,方便以后使用。
相对于移动端的首屏优化,PC在有些方面要苛刻得多,主要是因为PC端有太多的东西想要让用户看到,这就难免PC端的页面大而“重”,用户打开速度就会相对比较慢。用户体验不好。
1. 什么是首屏?
以800x600像素尺寸为标准,当浏览器加载页面后看到第一眼的显示内容为首屏。而从开始加载到浏览器页面显示高度达到600像素且此区域有内容显示的时间为首屏显示时间。
以京东首页为例:
当我们打开京东时,第一眼看到的内容即为首屏内容,也就是如上图的内容。
2. 为什么要做首屏优化
一个页面的“总加载时间”要比“首屏时间”长,但对于最终用户体验而言,当内容充满首屏的区域时,用户就可以看到网站的主要内容并可以进行各自的选择了。首屏时间的快与慢,直接影响到了用户对网站的认知度。
所以首屏时间的长短对于用户的滞留时间的长短、用户转化率都尤为重要。
3.京东是怎么做首屏优化的
在前端架构上大体还是沿用过去的架构,使用 jQuery + Seajs 这种古老的开发方式,因为首页还依赖着许多旧的系统与组件,无法在短时间内对基础架构进行升级,当然并不是说旧的就不好,要去盲目追求一些新的东西,而是这种架构还是有可以提升的地方。为了保证首屏速度,京东首屏依然采用直出的方式,优势就是1:SEO优化效果明显,2:首屏打开速度快。缺点就是:服务器压力大。于是对非首屏的楼层进行改进,使用前端模板 + 数据开发方式,将DOM字符串的渲染放到前端来做,后端只提供数据接口,以此来达到前后端分离的效果,同时在开发中使用假数据平台模拟接口,让前端工作不再依赖后端。
虽然使用前端模板 + 数据开发方式,会使得每个楼层都多一个接口,并且需要依靠JS来动态渲染,会影响到楼层加载的性能,但经过测试证明在现代PC浏览器下两种模式前端渲染和后端渲染并不会相差太多,并且在模板、数据双重缓存下,这样的差距更是微乎其微了,更关键的是能让我们的开发效率有所提升。
首屏直出是让首屏速度更快的最佳选择,此次版本依然选择了首屏直出的方式,直出的内容包括首屏HTML,页面楼层骨架,以及样式和一些必须的脚本,京东过去是将页面引用的所有样式都直出在页面上,没有外链的CSS,各种优化手段都考虑进去了,那么新版首页就只能在精简大小上下功夫了,所以新版首页的首屏只直出了首屏必须的样式,轮播图也只请求了第一张的数据。同时只直出一小部分必须的脚本,而非首屏的楼层样式拆分到各自楼层中,和楼层的模板放在一起,按需加载。
当我们打开京东的网站(不要滚动鼠标和键盘),右键查看源代码会发现京东首页的DOM树出奇的简单,页面DOM中多含有mod_lazyload的类,例如:
<div id="J_seckill" class="seckill seckill__loading mod_lazyload" aria-label="京东秒杀"></div>
再看下 localstorage,会发现它们的值中多存在一串完整的类似于html的内容
dom: '{%var i,clstagPrefix = pageConfig.clstagPrefix + o.staticLogTag;var isWide = pageConfig.compatible && pageConfig.wideVersion;%}{% var len = o.list.length; len = Math.min(len, 3); %}{% if (len >= 1) { %}<div class="grid_c1 rec_inner"><ul class="rec_list">{% for(i = 0; i < len; i++){ %}{% var item = o.list[i]; %}{% var imgUrl = isWide ? item.imgUrl : item.imgUrlB; %}<li class="rec_item" fclog="{%= item.clog %}"><a href="{%= item.url %}" class="rec_lk" target="_blank" clstag="{%= clstagPrefix + \'a\' + (i < 9 ? \'0\' : \'\') + (1+i) %}"><img src="//misc.360buyimg.com/mtd/pc/common/img/blank.png" data-lazy-img="{%= imgUrl %}" alt="{%= item.title %}" title="{%= item.title %}" class="rec_img" data-webp="no" ></a></li>{% } %}</ul></div>{% } %} ',
style: ".rec_list{overflow:hidden;height:100px}.rec_item{overflow:hidden;float:left;width:396px;height:100%}.rec_lk{display:block;height:100%}.rec_img{display:block;margin:auto}.o2_mini .rec_item{width:330px}.csstransitions .rec_img{-webkit-transition:opacity .2s;-moz-transition:opacity .2s;transition:opacity .2s}.csstransitions .rec_lk:hover .rec_img{opacity:.8}",
time: 1479195351434,
version: "ff78610a0ef9cdbb"
每个楼层的dom结构和css会缓存到localstorage中。通过版本号和时间戳来区分版本。在页面打开的时候,会将很多公共的css,js利用对比缓存缓存到本地。其中有一个js保存所有楼层依赖的js和css的版本号,把需要请求的路径写在 dom 上(例如:data-tpl="elevator_tpl"),用户滚动时,一旦该模块进入了视窗,则请求 dom 上对应的 data-tpl 地址,拿到渲染这个模块所需要的脚本和数据,不过这中间还有一层本地缓存 localstorage,如果在本地缓存中匹配到了对应的 hash string 内容,则直接渲染,否则请求到数据之后更新本地缓存。localstorage中的 version 会在页面加载时候,与后端文件 hash相对比,hash不变直接取localstorage中的内容(当然也可以使用cookie判断版本)。
分析到这里已经很明显了,通过前端缓存和异步加载jd已经完美的实现了首屏快速加载,在PC端达到了秒开的级别。
这里其实存在2个请求,1:楼层的初始化脚本(包含js和样式),2:楼层需要加载的数据。
为啥不把初始化楼层的脚本和加载数据的脚本放到一个js里面?还可以减少一次请求呢。为了让数据充分缓存下了不少功夫。数据的变化频率比较高,如果数据和初始化脚本包装在一起,虽然节约了一个请求,但一旦数据变化,整个脚本都得重新加载,而将数据和脚本分离,脚本可以长期缓存在本地,单独请求数据,这个量会小很多。直接改变上面的 version 版本号便可以让浏览器重新请求最新脚本。
从上面可以看出,任何一个模块的改动,在前端只会引起一个较小的加载变化,加上 http 的缓存策略,服务器的压力也是很小的。
4.还需要注意些什么?
看了上面的内容相信大家对于京东关于首屏优化的方案有了一个大体的了解,下面我们再整理一下关于首屏显示速度优化细节上的内容:
css静态文件在哪里?
为了求快,首页是没有css外链的,这样会再发起多次请求,相信对于我们来说,也是老生常谈的前端优化了。
那有人可能会问没有css外链,那如果一整个页面的css是否会增加页面的体积?其实上面就已经提到了,页面切分为模块化加载,对应模块下的css交给js或jsonp请求返回。整个页面的静态文件(图片,脚本,css)利用对比缓存缓存到本地。
js文件怎么加载?
京东采用请求合并的方式减少了与服务器交互的时间
<script src="//misc.360buyimg.com/??/jdf/lib/jquery-1.6.4.js,/jdf/2.0.0/ui/ui/1.0.0/ui.js,/mtd/pc/index/gb/lib.min.js,/mtd/pc/base/1.0.0/base.js,/mtd/pc/common/js/o2_ua.js,/mtd/pc/index/home/index.min.js,/mtd/pc/index/home/init.min.js"></script>
js如何执行?
懒执行,有交互才执行,楼层的js进入视口才会执行。有兴趣的可以看看小胡子哥的淘宝首页性能优化实践这篇文章
图片如何处理?
图片在其他屏(非首屏)都采用懒加载的模式,这样既能节省流量,也能减少请求数或延迟请求数。
首屏数据错误,白屏怎么办?
对于像京东首页这种大流量的网站,后端接口可能偶尔会出现错误,或者直接挂掉,特别是在双11这种可能会达到流量峰值的时候,但是不能因为接口出错的原因而使得页面显示出现错误。这就需要前端来配合给出一套合理的灾备方案。
通常,我们通过接口缓存、超时、重试来进行灾备处理。目前首页大部分接口、及所有模板请求,在请求成功后都会存入本地缓存,第二次请求,假如缓存没有过期将直接使用缓存,假如缓存过期将会重新请求,而一次正常的请求,都会经过超时或异常重试的逻辑,来保证用户能尽量访问到正常的数据,在正常接口无法获取数据之后又会有兜底接口来保障数据来源,这样的层层保障,很好地保证了页面的完整性。而且,针对所有接口,前端均有数据校验逻辑,每一个后端接口都要经过前端的数据校验,来验证接口的可用性,假如接口数据异常,前端将主动调用兜底接口来替代,这样来保证页面不至于错乱。
综上所述,首页的接口和模板正常请求流程如下
网络方面
web应用,总是会有一部分的时间浪费在网络连接和资源下载方面。往往建立一次网络连接是需要时间成本的。而且浏览器同一时间所发送的网络请求数是有限的。所以,这个层面的优化可以从「减少请求数目」开始:
-
减少http请求:在YUI35规则中也有提到,主要是优化js、css和图片资源三个方面,因为html是没有办法避免的。因此,我们可以做一下的几项操作:
合并js文件
合并css文件
雪碧图的使用(css sprite)
-
使用base64表示简单的图片
上述四个方法,前面两者我们可以使用webpack之类的打包工具进行打包;雪碧图的话,也有专门的制作工具;图片的编码是使用base64的,所以,对于一些简单的图片,例如空白图等,可以使用base64直接写入html中。
回到之前网络层面的问题,除了减少请求数量来加快网络加载速度,往往整个资源的体积也是,平时我们会关注的方面。
-
减小资源体积:可以通过以下几个方面进行实施:
gzip压缩
js混淆
css压缩
-
图片压缩
gzip压缩主要是针对html文件来说的,它可以将html中重复的部分进行一个打包,多次复用的过程。js的混淆可以有简单的压缩(将空白字符删除)、丑化(丑化的方法,就是将一些变量缩小)、或者可以使用php对js进行混淆加密。css压缩,就是进行简单的压缩。图片的压缩,主要也是减小体积,在不影响观感的前提下,尽量压缩图片,使用png等图片格式,减少矢量图、高清图等的使用。这样子的做法不仅可以加快网页显示,也能减少流量的损耗。
除了以上两部分的操作之外,在网络层面我们还需要做好缓存工作。真正的性能优化来说,缓存是效率最高的一种,往往缩短的加载时间也是最大的。
-
缓存:可以通过以下几个方面来描述:
DNS缓存
CDN部署与缓存
-
http缓存
由于浏览器会在DNS解析步骤中消耗一定的时间,所以,对于一些高访问量网站来说,做好DNS的缓存工作,就会一定程度上提升网站效率。CDN缓存,CDN作为静态资源文件的分发网络,本身就已经提升了,网站静态资源的获取速度,加快网站的加载速度,同时也给静态资源做好缓存工作,有效的利用已缓存的静态资源,加快获取速度。http缓存,也是给资源设定缓存时间,防止在有效的缓存时间内对资源进行重复的下载,从而提升整体网页的加载速度。
其实,网络层面的优化还有很多,特别是针对于移动端页面来说。众所周知,移动端对于网络的敏感度更加的高,除了目前的4G和WIFI之外,其他的移动端网络相当于弱网环境,在这种环境下,资源的缓存利用是相当重要的。而且,减少http的请求次数,也是至关重要的,移动端弱网环境下,对于http请求的时间也会增加。所以,我们可以看一下我们在移动端网络方面可以做的优化:
-
移动端优化:使用以下几种方式来加快移动端网络方面的优化:
使用长cache,减少重定向
首屏优化,保证首屏加载数据小于14kb
-
不滥用web字体
「使用长cache」,可以使得移动端的部分资源设定长期缓存,这样可以保证资源不用向服务器发送请求,来比较资源是否更新,从而避免304的情况。304重定向,在PC端或许并不会影响网页的加载速度,但是,在移动端网络不稳定的前提下,多一次请求,就多了一部分加载时间。「首屏优化」,对于移动端来说是至关重要的。2s时间是用户的最佳体验,一旦超出这个时间,将会导致用户的流失。所以,针对移动端的网络情况,不可能在这么短时间内加载完成所有的网页资源,所以我们必须保证首屏中的内容被优先显示出来,而且基于TCP的慢启动和拥塞控制,第一个14kb的数据是非常重要的,所以需要保证首部加载数据能够小于14kb。「不滥用web字体」,web字体的好处就是,可以代替某些图片资源,但是,在移动端过多的web字体的使用,会导致页面资源加载的繁重,所以,慎用web字体
渲染和DOM操作方面
首先,简单的聊一下优化渲染的重要性。在网页初步加载时,获取到HTML文件之后,最初的工作是构建DOM和构建CSSOM两个树,之后将他们合并形成渲染树,最后对其进行打印。我们可以通过图片来看一下,简单的过程:
这里整个过程拉出来写,具体可以再写一篇文章,恕我偷下懒,推荐一篇比较好的文章给大家吧。浏览器渲染过程与性能优化
继续我们的话题,我们可以如何去缩短这个过程呢?可以从以下几个操作进行优化。
-
优化网页渲染:
css的文件放在头部,js文件放在尾部或者异步
-
尽量避免內联样式
css文件放在「头部加载」,可以保证解析DOM的同时,解析css文件。因为,CSS(外链或内联)会阻塞整个DOM的渲染,然而DOM解析会正常进行,所以将css文件放在头部进行解析,可以加快网页的构建速度。假设将其放在尾部,那时DOM树几乎构建,这时就得等到CSSOM树构建完成,才能够继续下面的步骤。「js放在尾部」:js文件不同,将js文件放在尾部或者异步加载的原因是JS(外链或内联)会阻塞后续DOM的解析,后续DOM的渲染也将被阻塞,而且一旦js中遇到DOM元素的操作,很可能会影响。这方面可以推荐一篇文章——异步脚本载入提高页面性能。「避免使用内联样式」,可以有效的减少html的体积,一般考虑内联样式的时候,往往是样式本身体积比较小,往往加载网络资源的时间会大于它的时候。
除了页面渲染层面的优化,当然最重要的就是DOM操作方面的优化,这部分的优化应该是最多的,而且也是平时开发可以注意的地方。如果开发前期明白这些原理,同时付诸实践的话,就可以在后期的性能完善上面少下很多功夫。那么,接下来我们可以来看一下具体的操作:
-
DOM操作优化:
避免在document上直接进行频繁的DOM操作
对于加了动画的UI元素,设置position为absolute或fixed
如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document;
合并多次的DOM和样式的修改。并减少对style样式的请求
尽量使用css动画
-
使用事件代理
前面四个操作,其实都是希望『减少重排和重绘』。其实,进行一次DOM操作的代价是非常之大的,以前可以通过网页操作是否卡顿来进行判断,但是,现代浏览器的进步已经大大减少了这方面的影响。但是,我们还是需要清楚,如何去减少回流和重绘的问题。因为这里不想细说这方面的知识,想要了解的话,可以看这篇文章——回流与重绘:CSS性能让JavaScript变慢?。这可是张鑫旭大大的一篇文章呦(.)。「尽量使用css动画」,是因为本身css动画比较简单,而且相较于js的复杂动画,浏览器本身对其进行了优化,使用上面不会出现卡顿等问题。「使用requestAnimationFrame代替setInterval操作」,相信大家都有所耳闻,setInterval定时器会有一定的延时,对于变动性高的动画来说,会出现卡顿现象。而requestAnimationFrame正好解决的整个问题。「适当使用canvas」,不得不说canvas是前端的一个进步,出现了它之后,前端界面的复杂性也随之提升了。一些难以完成的动画,都可以使用canvas进行辅助完成。但是,canvas使用频繁的话,会加重浏览器渲染的压力,同时导致性能的下降。所以,适当时候使用canvas是一个不错的建议。「尽量减少css表达式的使用」,这个在YUI规则中也被提到过,往往css的表达式在设计之初都是美好的,但在使用过程中,由于其频繁触发的特性,会拖累网页的性能,出现卡顿。因此在使用过程中尽量减少css表达式的使用,可以改换成js进行操作。「使用事件代理」:往往对于具备冒泡性质的事件来说,使用事件代理不失为一种好的方法。举个例子:一段列表都需要设定点击事件,这时如果你给列表中的每一项设定监听,往往会导致整体的性能下降,但是如果你给整个列表设置一个事件,然后通过点击定位目标来触发相应的操作,往往性能就会得到改善。
DOM操作的优化,还有很多,当然也包括移动端的。这个会在之后移动端优化部分被提及,此处先卖个关子。上面我们概述了开始渲染的时候和DOM操作的时候的一些注意事项。接下来要讲的是一些小细节的注意,这些细节可能对于页面影响不大,但是一旦堆积多了,性能也会有所影响。
-
操作细节注意:
正确使用icon-font
在css属性为0时,去掉单位
禁止图像缩放
正确的css前缀的使用
移除空的css规则
对于css中可继承的属性,如font-size,尽量使用继承,少一点设置
-
缩短css选择器,多使用伪元素等帮助定位
上述的一些操作细节,是平时在开发中被要求的,更可以理解为开发规范。(基本操作,坐下_)
列举完基本操作之后,我们再来聊一下移动端在DOM操作方面的一些优化。
-
移动端优化:
长列表滚动优化
函数防抖和函数节流
使用touchstart、touchend代替click
HTML的viewport设置
-
开启GPU渲染加速
首先,长列表滚动问题,是移动端需要面对的,IOS尽量使用局部滚动,android尽量使用全局滚动。同时,需要给body添加上-webkit-overflow-scrolling: touch来优化移动段的滚动。如果有兴趣的同学,可以去了解一下ios和android滚动操作上的区别以及优化。「防抖和节流」,设计到滚动等会被频繁触发的DOM事件,需要做好防抖和节流的工作。它们都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。
介绍:函数防抖,当调用动作过n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间;函数节流,预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。
「touchstart、touchend代替click」,也是移动端比较常用的操作。click在移动端会有300ms延时,这应该是一个常识呗。(不知道的小伙伴该收藏一下呦)。这种方法会影响用户的体验。所以做优化时,最简单的方法就是使用touchstart或者touchend代替click。因为它们事件执行顺序是touchstart->touchmove->touchend->click。或者,使用fastclick或者zepto的tap事件代替click事件。「HTML的viewport设置」,可以防止页面的缩放,来优化性能。「开启GPU渲染加速」,小伙伴们一定听过CPU吧,但是这里的GPU不能和CPU混为一谈呦。GPU的全名是Graphics Processing Unit,是一种硬件加速方式。一般的css渲染,浏览器的渲染引擎都不会使用到它。但是,在3D渲染时,计算量较大,繁重,浏览器会开启显卡的硬件加速来帮助完成这些操作。所以,我们这里可以使用css中的translateZ设定,来欺骗浏览器,让其帮忙开启GPU加速,加快渲染进程。
DOM部分的优化,更多的是习惯。需要自己强制要求自己在开发过程中去注意这些规范。所以,这部分的内容可以多关注一下,才能够慢慢了解。同时,本人对于上述几点的描述是概括性的。并没有对其进行详细的展开。因此,也要求你去细细的查阅Google呦。
数据方面
数据,也可以说是前端优化方面比较重要的一块内容。页面与用户的交互响应,往往伴随着数据交互,处理,以及ajax的异步请求等内容。所以,我们也可以来聊聊这一块的知识。首先是对于图片数据的处理:
-
图片加载处理:
图片预加载
图片懒加载
-
首屏加载时进度条的显示
「图片预加载」,预加载的寓意就是提前加载内容。而图片的预加载往往会被用在图片资源比较大,即时加载时会导致很长的等待过程时,才会被使用的。常见场景:图片漫画展示时。往往会预加载一张到两张的图片。「图片懒加载」,懒加载或许你是第一次听说,但是,这种方式在开发中会被经常使用。首先,我们需要明白一个道理:往往只有看到的资源是必须的,其他资源是可以随着用户的滚动,随即显示的。所以,特别是对于图片资源特别多的网站来说,做好图片的懒加载是可以大大提升网页的载入速度的。
常见的图片懒加载的方式就是:在最初给图片的src设置一个比较简单的图片,然后将图片的真实地址设置给自定义的属性,做一个占位,然后给图片设置监听事件,一旦图片到达视口范围,从图片的自定义属性中获取出真是地址,然后赋值给src,让其进行加载。
「首屏进度条的显示」:往往对于首屏优化后的数据量并不满意的话,同时也不能进一步缩短首屏包的长度了,就可以使用进度条的方式,来提醒用户进行等待。
讲完了图片这一块数据资源的处理,往往我们需要去优化一下异步请求这一部分的内容。因为,异步的数据获取也是前端不可分割的。这一部分我们也可以做一定的处理:
-
异步请求的优化:
使用正常的json数据格式进行交互
部分常用数据的缓存
-
数据埋点和统计
「JSON交互」,JSON的数据格式轻巧,结构简单,往往可以大大优化前后端的数据通信。「常用数据的缓存」,可以将一些用户的基本信息等常用的信息做一个缓存,这样可以保证ajax请求的减少。同时,HTML5新增的storage的内容,也不用怕cookie暴露,引起的信息泄漏问题。「数据埋点和统计」,对于资深的程序员来说,比较了解。而且目前的大部分公司也会做这方面的处理。有心的小伙伴可以自行查阅。
最后,还有就是大量数据的运算。对于javascript语言来说,本身的单线程就限制了它并不能计算大量的数据,往往会造成页面的卡顿。而可能业务中有些复杂的UI需要去运行大量的运算,所以,webWorker的使用是至关重要的。或许,前端标准普及的落后,会导致大家对于这些新生事物的短暂缺失吧。
总结
本篇文章就前端性能这个话题做了一个总结。或许,并不全面,但是都是一些平时开发中会被经常用到的知识。希望有心者能够去亲身的尝试一下这些方面的优化。本篇的概述了一下几个知识点:
- 首屏优化
- 网络层面的优化
- 数据层面的优化
- DOM操作与渲染层面的优化
另外,写代码的时候注意代码规范。
京东UED代码规范
参考:
https://juejin.im/post/59e1bbc9f265da430f311fb1