移动web性能优化从入门到进阶--进阶篇

2,095 阅读25分钟

1.资源合并与压缩

启用GZIP

gzip是GNUzip的缩写,最早用于UNIX系统的文件压缩。HTTP协议上的gzip编码是一种用来改进web应用程序性能的技术,web服务器和客户端(浏览器)必须共同支持gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如Apache,Nginx,IIS同样支持gzip。 gizp流程

  • 浏览器请求url,并在request header中设置属性accept-encoding:gzip。
  • 服务器支持gzip,response headers返回包含content-encoding:gzip。
  • 开启gzip可以达到80%的压缩率,即1MB的文件下载下来只需要200K,大大减少传输效率,是一项非常重要的资源压缩手段。

Nginx中开启gzip:

图片描述

升级HTTP/2.0

HTTP/2是HTTP协议自1999年HTTP 1.1发布后的首个更新,主要基于SPDY协议(是Google开发的基于TCP的应用层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验)。 优化原理: 根据上文中说的资源合并问题,浏览器可以同时建立有限个TCP连接,而每个连接都要经过慢启动三次握手连接建立,HTTP1.1为了解决这个问题推出了keep-alive,即保持连接不被释放,但是真正的这些连接下载资源是一个线性的流程:一个资源的请求响应返回后,下一个请求才能发送。这被称为线头阻塞,为了彻底解决此问题,HTTP2.0带来了多路复用

图片描述
HTTP2.0的其他新特性也有助于页面的打开速度:

合并资源 vs 并行加载资源?

现在回过头来探讨一下上文说的资源合并问题,有了HTTP2.0之后,我们是否还需要合并资源,目前看需要遵循下面的原则:

  • 停止合并文件 在HTTP/1.1中,CSS,JavaScript被压缩到了一个文件,图片被合并到了一张雪碧图上。合并CSS、JavaScript和图片极大地减少了HTTP的请求数,在HTTP/1.1中能获得显著的性能提升。 但是,在HTTP/2.0中合并文件不再是一个好的办法。虽然合并依然可以提高压缩率,但它带来了代价高昂的缓存失效。即使有一行代码改变了,整个文件就要重新打包压缩,浏览器也会强制重新加载新的文件。

  • 尽量不要在HTML里内联资源 非特殊的代码(rem适配代码,上报代码等)之外,尽量不要使用内联资源,在极端情况下,这确实能够减少给定网页的HTTP请求数。但是,和文件合并一样,HTTP/2优化时你不应该内联文件。内联意味着浏览器不能缓存单个的资源。如果你将所有页面使用的CSS声明嵌入了每一个HTML文件,这些文件每次都要从服务端获取。这导致用户在访问任何页面时都要传输额外的字节。

  • 合并域名 拆分域名是让浏览器建立更多TCP连接的通常手段,浏览器限制了单个服务器的连接数量,但是通过将网站上的资源切分到几个域上,你可以获得额外的TCP连接,但是每个拆分的域名都会带来额外的DNS查询、握手,新连接的建立,根据HTTP2.0多路复用的原则:HTTP2采用多路复用是指,在同一个域名下,开启一个TCP的connection,每个请求以stream的方式传输,域名的合并可以带来更多的多路复用,如下图在chrome的Network面板中查看HTTP2.0,注意protocol和ConnectID相同则表示启用复用:

    图片描述

合理使用icon类图片base64化

<img src="data:image/gif;base64,R0..."/>

在页面使用的背景类图片icon类图片,不多且比较小的情况下,可以把图片转成base64编码嵌入到html页面或者CSS文件中,这样可以减少页面的HTTP请求数。需要注意的是,要保证图片较小,一般超过5kb的就不推荐base64嵌入显示了。为什么是5kb?。 同时,采用Webpack的url-loader可以帮我们在不影响代码可读性的情况下,解决base64字符串问题。

Icon Font

IconFont技术起源于Web领域的Web Font技术,它是把一些简单的图标制作成字体,然后让图标变成和字体一样使用,Icon 的设计和使用在近几年的发展中,也经历了由当初的 img 方案 到现如今的 svg 方案,有以下优点:

  • 字体是矢量的,所以可以随意改变大小。
  • 因为它是字体,所以所有字体的css都可以使用,比如font-size,color,background,opacity等。
  • 减少图片请求数。
  • iconfont没有兼容性问题,IE6,Android2.3都能够兼容。

2.浏览器加载原理优化

首屏资源优化

1)剥离首屏资源:

首屏的快速显示,可以大大提升用户对页面速度的感知,因此应尽量针对首屏的快速显示做优化,基于联通3G网络平均338KB/s(2.71Mb/s),所以首屏资源不应超过1014KB,剥离首屏需要的资源,非首屏的资源单独合并,采用懒加载。这个原则适用上文的资源合并和加载中的场景。

2)按需加载:

将不影响首屏的资源和当前屏幕资源不用的资源放到用户需要时才加载,可以大大提升重要资源的显示速度和降低总体流量,对于移动web端常见的多tab页面,Webpack的CodeSplitting帮助我们更加便捷实现按需加载。

3)非首屏图片Lazyload:

不用多说,在目前流量费用还算比较高昂的情况下,帮助用户节省更多的流量可以避免用户的投诉,为了保证页面内容最小化,加速页面渲染,尽可能节省首屏网络流量,页面中的图片资源推荐使用懒加载实现,在页面滚动时动态载入图片。

使用CDN

CDN是将源站内容分发至最接近用户的节点,使用户可就近取得所需内容,提高用户访问的响应速度和成功率。解决因分布、带宽、服务器性能带来的访问延迟问题,适用于站点加速、点播、直播等场景。 对于web页面来说,将项目的js,css等静态资源存放在CDN是一个重要的优化手段,加入所有资源统一打包放在同一个域名下,很难达到用户就近获取的优势(目前最佳实践是html页面采用一个域名,静态资源文件采用CDN域名),所谓静态资源即是可以被浏览器缓存的资源,而对于html页面,由于是js和css等链接的入口,通常不采用缓存。常用的阿里云CDN腾讯云CDN都有开放接口,开发者可以按需选择。

预加载

此预加载主要分为两个部分,一种是采用原生浏览器支持的API来对页面的一些资源进行预先拉取或者加载,另一种是通过自己写逻辑来加载一些重要的资源,立即下面内容的前提是要立即目前移动web常见的hybrid架构,webview外壳+H5页面:

图片描述

1) DNS预解析(dns-prefetch):

DNS 作为互联网的基础协议,其解析的速度似乎很容易被网站优化人员忽视。现在大多数新浏览器已经针对DNS解析进行了优化,典型的一次DNS解析需要耗费 20-120 毫秒,减少DNS解析时间和次数是个很好的优化方式。DNS Prefetching 是让具有此属性的域名不需要用户点击链接就在后台解析,而域名解析和内容载入是串行的网络操作,所以这个方式能 减少用户的等待时间,提升用户体验 。 html <link rel="dns-prefetch" href="//haitao.nos.netease.com">

2) Preload 和 Prefetch:

两者都是以<link rel="preload"> 和 <link rel="prefetch">作为引入方式。

Preload

一个基本的用法是提前加载资源,告诉浏览器预先请求当前页需要的资源,从而提高这些资源的请求优先级,加载但是不运行,占用浏览器对同一个域名的并发数:

<link rel="preload" href="a.js" as="script" onload="preloadLoad()">
Prefetch

用法是浏览器会在空闲的时候,下载资源, 并缓存起来。当有页面使用的时候,直接从缓存中读取。其实就是把决定是否和什么时间加载这个资源的决定权交给浏览器。

   <link rel="prefetch" href="a.js">

遗憾的是对于这两个接口,移动端的浏览器支持性很不好,这也是没有普遍推广开来的原因。

图片描述
图片描述
图片描述
什么时候使用Preload,什么时候使用Prefetch可以总结如下:对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch。关于Preload 和 Prefetch可以参考这里

另外还有PrerendersubresourcePreconnect属性,由于目前能支持到这些属性的机型太少,这里就不在赘述了。

3) 业务逻辑的预加载:

关于业务逻辑的预加载,在这里我可以举一个微信小程序的例子。小程序主要分为渲染层和逻辑层,逻辑层有iOS或者Android的JavaScriptcore来运行,渲染层由各自的webview组件负责渲染。我们用户实际体验到的UI还是跑在我们的webview里面,这个和大多数H5页面的渲染用的是一个组件。但是为什么我们体验小程序会比H5页面要快很多?尤其是新开页面时?

图片描述
小程序在启动时,会预先加载所有页面逻辑代码进内存,在 a页面跳转至 b页面 时,可以在内存中直接运行而无需在发送资源请求,a页面的逻辑代码 Javascript 数据也不会从内存中消失。b页面甚至可以直接访问 a页面中的数据,整个坏境在一个大的上下文中。 当然这里你可能会有疑问?假如用户不会进入page2,那加载page2的逻辑代码岂不是浪费?这里就会牵扯到一个用户行为预测的问题,在小程序的架构中,整个逻辑代码是统一在一个包里,微信是统一将这些文件下载并加载到内存中,这可能会涉及到一些浪费,但是对于提速来讲收益大于弊端的。当然小程序页提供出分包策略来优化这些问题。

借助小程序的思路,我们的移动web同样适用这种预加载优化逻辑:

预加载资源:

在多tab的单页应用中,我们可以在用户打开首屏之后,预先加载其他tab的资源。例如用户进入时在推荐tab,这时就可以预先加载订单,我的 这两个tab的资源了,当用户点击订单时,页面的展现就会快一些。

图片描述

预加载数据:

预加载数据的时机最好是在空闲时,什么是空闲时呢?我们分析一下打开一个H5页面的流程:

图片描述
从图上可以看到,利用闲时可以做的事情有很多,预加载数据是一个典型的优化手段,提前把新页面所需要的数据加载好,在新页面打开后,可以直接用数据来进行渲染,当然这里涉及到的跨页面数据通信,我们可以利用localStroage来实现。

预加载webview:

我利用闲时来做更多事情的前提是闲时够长,但这本书也不是一个很好的现象,尽量的减少闲时,也是我们需要做的一项优化,例如我们来减少webview的加载时间,这就需要提前加载webview,此项优化大多是由native端来完成:

  • 在APP启动后,就提前在内存中将webview加载好,而不是等到点击进入web页面时才去加载。
  • 创建一个webview的复用池,例如最多只存在3个webview,每次从池子里获取webview,达到复用的目的。

3.合理利用缓存

上文说了浏览器缓存的基础知识,既然是基础,那就说明必须掌握,下面来说一些进阶篇的利用缓存来优化页面:

妙用localStorage

HTML5 LocalStorage可以看做是加强版的cookie,数据存储大小提升,有更好的弹性以及架构,可以将数据写入到本机的ROM中,还可以在关闭浏览器后再次打开时恢复数据,以减少网络流量,日常使用localStorage来优化我们的页面大概有以下几种场景:

  • 缓存一些非实时更新的变量,例如某些闪屏的标志位信息,地理位置信息等等,取用方便,即存即用。
  • 使用localStorage缓存Js和css文的,为了提升页面的打开速度,或者是页面可以离线使用,有些页面会采用将静态资源文件直接缓存在localStorage中,当页面打开时将内容读取出并运行,使用此方法确实可以减少http请求,提速页面。
  • 在一些跨webview通信的场景中,localStorage是兼容性最好的数据通信方案,例如预加载的数据可以缓存在localStorage中,来实现各个页面的webview数据共享。
  • 需要注意的是,localStorage并不是无限大的,针对每个域名,PC端浏览器给localStorage分配的容量大概4.5m-5m,移动端类似微信等等的浏览器大概容量是2.5m-3m参考这里。所以在使用时需要做好异常捕获,让localstorage超出容量时,是无法在进行插入并报错,如果对容量有更高的要求,可以参考使用indexeddb。需要注意的是indexeddb的兼容性却不是很好,android4.4之前以及iOS7以前都无法使用。

老生常谈离线包

离线包技术可以说是并不算很新的技术了,各个业务都有在使用,也都有自己的一套hybrid离线包系统,关键点在于离线包的打包,同时对文件加密/签名,更新离线包(增量) ,安全教研以及容错机制等等,在这里列举一些大厂的离线包方案来参考:

Service Worker探索

提到缓存,那就不得不提近几年比较火的Service Worker了:

图片描述
作为一个比较新的技术,大家可以把 Service Worker 理解为一个介于客户端和服务器之间的一个代理服务器。在 Service Worker 中我们可以做很多事情,比如拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的作用之一就是离线资源缓存。 Service Worker的主要复杂点在于不断地对缓存策略的调整,笔者在这里就不过多展开,可以参考一下淘宝Service worker实战

4.Nodejs服务端渲染(SSR)优化首屏时间

在前后端分离之后,后端语言的模板功能被弱化,整个页面的渲染基本上都由前端 js 动态渲染,但这样对于一些应用来说是有缺陷的。比如需要 SEO 的,需要打开页面不用等待就能看到页面的,另外前端页面展示过度依赖js和css逻辑执行,在极端情况或者网络较差,手机性能低下(尤其在低端Android机型较为明显)时,白屏时间较长,这时服务端渲染便应用而生,至于为什么是Nodejs,作为一个前端,难道还要用Java么。。?

为什么会有服务端渲染?

如果你说服务端渲染和早期web框架,例如SSH,JSP servlet,PHP等等一样的话,那我只能说呵呵,目前的服务端渲染和早期的框架是有本质区别的:

  • Web 2.0时代最大的思想革命本质不是前后端分离,而是把网页当作独立的应用程序(app)。建立在前后端分离的基础上,后端只负责提供数据json格式,前端还是负责页面交互逻辑,大多数的服务端渲染采用Nodejs层来进行数据组装,html拼接。
  • 重点在首屏!!首屏时间的优化,移动互联网时代的爆发,用户对网页性能的要求越来越高,但毕竟基于3G,4G网络,让用户更快的看到页面就能挽留更多的潜在商机。服务端只负责首屏的页面渲染,真正过了首屏,大多数的业务逻辑,页面交互,还是需要有单独的前端来实现的。

如何实现?

如果你的项目用的是React或者是vue,那么下面两个现场的开源框架是不错的选择。

当然,你也可以自己实现一套自己的服务端渲染框架,一般需要关注这些问题:

  • 实现自定义的Node端的window上下文对象Cookie & Session等。
  • 远程数据的获取,一般采用Nodejs的http模块。
  • React采用ReactDOMServer调用renderToString(),Vue采用vue-server-renderer调用renderToString()。
  • Node端内存泄露和控制等问题

代码同构

使用Nodejs的服务端渲染的一大优势就是代码同构,这使得一个项目可以分别部署成走线上正常前端渲染版本,和走服务端渲染版本,这样可以更好的做到容灾机制,当任何一种分之挂掉之后,可以直接走另一个版本,提高稳定性。这也同构的魅力所在!因为在同构直出宕掉的时候,还有前端渲染页面可以提供正常的服务。

图片描述

取舍

虽然说服务端渲染这类优化确实可以提升一定的页面首屏时间,但是也是需要成本的,在前端开发接管了Node作为中间层时,需要额外的机器资源部署,并且一旦接触到后端,容灾机制,内存管理等性能指标都需要关注,这对于当前的业务系统架构可能需要有一定的调整,所以还是要斟酌来使用。

5.渲染优化

终于回到我们前端的老本家了,如果说前面的优化都是在框架,逻辑层面的优化,或者是参考后端,客户端的优化思路,那么真正涉及到UI渲染的优化才是我们作为前端工程师的立身之本了。

何为渲染优化?

抛开首屏加速,真正让用户体验web页面的另一个很重要的部分就是用户行为交互了,这包括用户的点击相应滚动流畅度动画是否卡顿流畅度等等,这些关于用户交互性的优化在已往的PC端可能不是很被重视,因为PC浏览器的性能要远远大于手机端,但是到了移动web就不一样了,用户都希望移动web能有PC端一样的性能。

为什么同样的页面在iPhone里总比Android流畅?

目前主流的Android硬件配置可以说是甩iPhone几条街了,那为什么高配置却得不到好的体验呢?关键两类机型的操作系统上的优化程度,其中一个原因就是iOS操作系统采用执行率较高的Object-c语言,大部分硬件接口可以直接调用和运行,而Android则采用Java语言,因为虚拟机的存在,虽然跨平台性提升了,但是通过虚拟机在和系统硬件交互,执行效率就低了很多,当然这只是其中一个原因。那么,我们移动web主要优化的群体就是Android机型了。

16ms优化

目前大多数设备的屏幕刷新频率为60次/秒,每一帧所消耗的时间约为16ms(1000 ms / 60 = 16.66ms),这16ms就是渲染帧的时长,所谓渲染帧是指浏览器一次完整绘制过程,帧之间的时间间隔是DOM视图更新的最小间隔,但实际上,浏览器还有一些整理工作要做,因此开发者所做的所有工作需要在10ms内完成。 如果不能完成,帧率将会下降,网页会在屏幕上抖动,也就是通常所说的卡顿,这会对用户体验产生严重的负面影响。所以如果一个页面中有动画效果或者用户正在滚动页面,那么浏览器渲染动画或页面的速率也要尽可能地与设备屏幕的刷新频率保持一致,以保证良好的用户体验。在这一个间隔内,浏览器可能需要做以下事情:

 - 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等
 - 样式计算(CSS Object Model):级联地生成每个节点的生效样式。
 - 布局(Layout):计算布局,执行渲染算法
 - 重绘(Paint):各层分别进行绘制(比如 3D 动画)
 - 合成(Composite):合成各层的渲染结果

16ms优化重绘和重排

在上面浏览器需要做的这些事情中,会引发不同程度的重绘和重排,而重绘和重排正式影响流畅的重要因素:

  1. 部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算,这被称为重排。

  2. 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新,这样的更新被称为重绘。

重排和重绘代价是高昂的,它们会破坏用户体验,并且让UI展示非常迟缓,但是每次重排,必然会导致重绘,而每次重绘并不一定会发生重排,我们需要在以下几种场景来减少重排的发生: 当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:

  1. 添加或者删除可见的DOM元素。
  2. 元素位置改变。
  3. 元素尺寸改变——边距、填充、边框、宽度和高度。
  4. 内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变。
  5. 页面渲染初始化。
  6. 浏览器窗口尺寸改变——resize事件发生时。

使用 requestAnimationFrame:

提升动画流畅度的另一个重要因素是让浏览器变得智能起来,好在浏览器给我们提供了这个接口requestAnimationFrame,通过这个API,可以告诉浏览器某个JavaScript代码要执行动画,浏览器收到通知后,则会运行这些代码的时候进行优化,它会确保JS尽早在每一帧的开始执行,实现流畅的效果,而不再需要开发人员烦心刷新频率的问题了:

	function animationWidth() {
	  var div = document.getElementById('box');
	  div.style.width = parseInt(div.style.width) + 1 + 'px';

	  if(parseInt(div.style.width) < 200) {
	    requestAnimationFrame(animationWidth)
	  }
	}
	requestAnimationFrame(animationWidth);

试试requestIdleCallback

requestIdleCallback的出现伴随着React 16 的Fiber特性,他的使用场景是当用户在做负责交互时,不希望因为一些不重要的任务(如统计上报)导致用户感觉到卡顿的话,就应该考虑使用了,因为requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态,但是需要注意的是不要在requestIdleCallback操作任何DOM,这违背了这个接口的设计原则。

	     requestIdelCallback(myNonEssentialWork);
	    function myNonEssentialWork (deadline) {
	    
	      // deadline.timeRemaining()可以获取到当前帧剩余时间
	      while (deadline.timeRemaining() > 0 && tasks.length > 0) {
	        doWorkIfNeeded();
	      }
	      if (tasks.length > 0){
	        requestIdleCallback(myNonEssentialWork);
	      }
	    }

fragment元素的应用

在你使用dominnerHTML方法来插入大量dom节点时,不妨试试fragment,fragment文档片段是个轻量级的document对象,它的设计初衷就是为了完成这类任务——更新和移动节点。文档片段的一个便利的语法特性是当你附加一个片断到节点时,实际上被添加的是该片断的子节点,而不是片断本身。只触发了一次重排,而且只访问了一次实时的DOM。

列表滚动优化

长列表滚动在移动端是一种非常常见的交互模式,例如feeds流,图片流等等,这些列表的滚动流畅度优化对用户体验的提升是非常重要的,基于目前的优化思路,借助dom复用的方案,类似iOS的UITableView或者Android的recyclerview原理,在列表滚动时,只保证视窗区域内的dom节点存在,在有限的dom节点内实现滚动,而不在创建新的节点,在用户不断下拉翻页的过程中,保证整个页面有限的dom元素来减少内存的消耗,原理如下图:

图片描述
复用的dom:
图片描述
采用这一个方案的前端是借助浏览器的onscroll事件来做逻辑处理,但是问题在于有些机型例如iOS的UIWebview下,onscroll不能实时触发,这就给优化带来了难题,由此引发出了模拟滚动:

  • 正常的滚动:我们平时使用的scroll,包括上面讲的滚动都属于正常滚动,利用浏览器自身提供的滚动条来实现滚动,底层是由浏览器内核控制。
  • 模拟滚动:最典型的例子就是iscroll了,原理一般有两种:
    • 1).监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,然后采用requestanimationframe来在一个线型函数下不断的修改元素的transform来实现手指离开时的一段惯性滚动距离。
    • 2).监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,然后给元素一个css的animation,并设置好duration和function来实现手指离开时的一段惯性距离。

结论是如果要采用模拟滚动,可以解决onscroll不实时触发的问题,从而实现长列表的复用的优化,但是带来新的问题就是模拟滚动本身也是dom的重绘,增加额外的性能消耗,达到有优化效果并不理想,好在iOS的新版WKwebview解决了onscroll问题,让开发者有了更好的选择。

滚动函数防抖

当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。如下图,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。

	function debounce(fn, wait) {
	    var timeout = null;
	    return function() {
	        if(timeout !== null) 
	                clearTimeout(timeout);
	        timeout = setTimeout(fn, wait);
	    }
	}
	// 处理函数
	function handle() {
	    console.log(Math.random()); 
	}
	// 滚动事件
	window.addEventListener('scroll', debounce(handle, 1000));

合理使用GPU

动画卡顿是在移动web开发时经常遇到的问题,解决这个问题一般会用到CSS3硬件加速。硬件加速这个名字感觉上很高大上,其实它做的事情可以简单概括为:通过GPU进行渲染,解放CPU,我们可以利用GPU的图形层,将负责的动画操作放在这个层,如何开启?

   webkit-transform: translateZ(0);

强制把需要动画的dom的对象 ,放置在GPU的layout层来缓存从而达到任何移动,大小变化都在这个层。 通过开启GPU硬件加速虽然可以提升动画渲染性能或解决一些棘手问题,但使用仍需谨慎,使用前一定要进行严谨的测试,否则它反而会大量占用浏览网页用户的系统资源,尤其是在移动端,肆无忌惮的开启GPU硬件加速会导致大量消耗内存,千万不要* {webkit-transform: translateZ(0);}

写在最后

本文在性能优化的基础上,将移动web的性能点逐步展开和深入,内容比较多,期望各位开发者能真正实践并进行不断尝试,总之:

技术就是在于不断折腾,愿各位在踩坑的道路上一帆风顺!

最后,向大家推荐一门慕课网的实战课程《移动Web APP开发之实战美团外卖》(立即学习),希望小伙伴们能通过这门课程收获满满,祝大家学习进步。