钉钉的 H5 性能优化方案
USASam
8年前
<p>对于一个H5的产品,功能无疑很重要,但是性能同样是用户体验中不可或缺的一环。原本H5的渲染性能就不及native的app,如果不把性能优化做起来,将极大地影响用户使用产品的积极性。</p> <h2><strong>用户感受</strong></h2> <p>当用户能够在1-2秒内打开H5页面,看到信息的展示,或者能够开始进行下一步的操作,用户会感觉速度还好,可以接受;而页面如果在2-5秒后才进入可用的状态,用户的耐心会逐渐丧失;而如果一个界面超过5秒甚至更久才能显示出来,这对用户来说基本是无法忍受的,也许有一部分用户会退出重新进入,但更多的用户会直接放弃使用。</p> <h2><strong>一秒钟法则</strong></h2> <p>移动互联网的架构、通讯机制,与有线网络有着巨大的差异,这也给H5的开发带来了很大的挑战。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/1a80dbb588dafb465825f1aed65bfb6e.jpg"></p> <p>这是一张手机端接入服务器的流程。</p> <p>首先,手机要通过无线网络协议,从基站获得无线链路分配,才能跟网络进行通讯。 无线网络基站、基站控制器这方面,会给手机进行信号的分配,已完成手机连接和交互。 获得无线链路后,会进行网络附着、加密、鉴权,核心网络会检查你是不是可以连接在这个网络上,是否开通套餐,是不是漫游等。核心网络有SGSN和GGSN,在这一步完成无线网络协议和有线以太网的协议转换。 再下一步,核心网络会给你进行APN选择、IP分配、启动计费。 再往下面,才是传统网络的步骤:DNS查询、响应,建立TCP链接,HTTP GET,RTTP RESPONSE 200 OK,HTTP RESPONSE DATA,LAST HTTP RESPONSE DATA,开始UI展现。</p> <p>可见,通过运营商的网络上网,情况比较复杂,经过的节点太多;运营商的网络信号强度变化频繁,连接状态切换快;网络延迟高、丢包率高;网络建立连接的代价高,传输速度快慢不等(从2G到4G,相差很大)。</p> <p>而我们优化的目标,就是所谓的一秒钟法则,即达成以下的标准:</p> <ul> <li> <p>2g网络:1秒内完成dns查询、和后台服务器建立连接</p> </li> <li> <p>3g网络:1秒内完成首字显示(首字时间)</p> </li> <li> <p>wifi网络:1秒内完成首屏显示(首屏时间)</p> </li> </ul> <h2><strong>优化方案</strong></h2> <h3><strong>★ 资源加载</strong></h3> <p>首屏加载</p> <p>用户从点击按钮开始载入网页,在他的感知中,什么时候是“加载完成”?是首屏加载,即在可见的屏幕范围内,内容展现完全,loading进度条消失。因此在H5性能优化中,一个很重要的目的就是尽可能提升这个“首屏加载”的时间,让它满足“一秒钟法则”。</p> <p><strong>按需加载 </strong></p> <p>首先要明确,按需加载虽然能提升首屏加载的速度,但是可能带来更多的界面重绘,影响渲染性能,因此要评估具体的业务场景再做决定。</p> <p><strong>Lazyload</strong></p> <p>Lazyload,即延迟加载,这并不是一个新的技术,在PC时代也是非常常用的一种性能优化手段。这个方案的原则是让屏幕外,或者不影响整体效果显示的图片、背景等资源,在界面就绪之后再进行网络加载。</p> <p><strong>滚屏加载</strong></p> <p>滚屏加载是一种常见的无刷新动态加载数据的方案,通常用在列表形式数据展示中。一方面,数据不是通过翻页进行加载,这样就避免了再一次请求和渲染整个页面;另一方面,数据显示的数量是受限的,例如第一次只请求了10条数据,也就只需要渲染这10条数据,下拉滚屏的时候,再去获得下面10条数据。</p> <p><strong>Media Query(响应式加载)</strong></p> <p>响应式设计是现在网站设计的一个流行趋势,随着移动互联网的发展,这项技术也越来越受到重视。通过这项技术,我们能够方便地控制资源的加载与显示,例如说在分辨率不同的手机上,分别使用不同的css,加载不同大小的图片资源。 方案参考:http://www.poluoluo.com/jzxy/201206/167034.html</p> <p><strong>第三方资源异步加载</strong></p> <p>第三方资源有的时候不可控,比如说页面统计、地图显示、分享组件等,这些第三方资源使用的时候要慎重选择,充分考察它们对于性能的影响,使用异步加载的方式进行,防止第三方资源的使用影响到页面本身的功能。</p> <p><strong>Loading进度条</strong></p> <p>在加载时间较长的时候,务必要让用户明确感知到加载完成的提示,通常是在加载过程中显示Loading的进度条,加载完成的时候隐藏它。从心理上,这会让用户有一种“期盼感”,而不至于太过枯燥。</p> <p>对于一些重量级的H5应用,例如游戏,开始前需要加载很多资源才能让后面的游戏过程更为流畅,一个带百分比进度显示的进度条就更加重要。</p> <p><strong>避免30*/40*/50*的http status</strong></p> <ul> <li> <p>200是一个正常的response,我们在浏览器中打开一个网页(后面会讲如何针对移动端进行调试),还会看到304,即命中浏览器缓存。这两种状态是正常的http status。</p> </li> <li> <p>302、301跳转是常见的跳转,尤其前一种,在我们进行鉴权的时候有时会用到,但这个做法要尽可能地优化,一个页面访问,最多只进行一次302跳转即可,切忌频繁地跳转。</p> </li> <li> <p>404、500,我们对自己开发的代码比较注意,一般不会发生,但是有的时候,加载第三方库,尤其是第三方库中有自己load组件的操作,这时,404和500错误可能会在你不知不觉的时候发生。例如钉钉的第三方微应用中,就遇到过dojo的组件加载问题,加载的一些子组件失败了,但是又没有影响页面显示,这就很容易被忽略。后面也会再讲,如何去测试和发现这样的隐患。</p> </li> </ul> <p><strong>Favicon.ico</strong></p> <p>如果我们没有设置图标ico,则会加载默认的图标:域名目录下的favicon.ico。很多开发者没有注意到这一点,就会导致这个请求404或者500。</p> <p>通常,我们在应用内部打开网页,不会显示这个图标出来(除非放到浏览器中显示网页),我们需要保证这个图标存在,尽可能地小(一般4KB以下),并且设置一个较长的缓存过期时间。</p> <p><strong>★图片的使用</strong></p> <p><strong>格式选择</strong></p> <p>显示效果较好的图片格式中,有webp、jpg和png24/32这几种常见的图片格式。一般来说,webp的图片最小,但在iOS或者android4.0以下的系统中可能会有兼容性问题需要解决。</p> <ul> <li> <p>Jpg是我们最常使用的方案,大小适中,解码速度快,兼容性问题也基本不存在,是我们在H5的应用中使用起来性价比最高的方案。</p> </li> <li> <p>Png24或png32,一般来说,显示效果肯定会比jpg更好,但是实际上人眼很难感知出来,所以在H5应用中要避免这种格式的大图片。</p> </li> </ul> <p>对于少量的图片,推荐用智图或者tinypng等工具来帮助自己选择合适的大小、格式。</p> <p><strong>像素控制</strong></p> <p>在H5应用中,图片的像素要严格控制,一般来说不建议宽度超过640px。</p> <p><strong>小图片合并</strong></p> <p>在html网页中,如果有多个小图片需要加载,不妨试试CSS Sprites方案,尤其是一些基本不变,大小差不多的操作类型图标。</p> <p>避免html代码中的大小重设</p> <p>在html或者css中,如果有类似width: **px这样的代码,就要注意看一看了,如果说图片显示的效果是宽度100px,而下载的图片却是200px宽度,那这大小基本上就是所需要的4倍面积了,所以在H5应用中,使用图片的一个原则就是需要显示成多大,就下载多大的资源。</p> <p><strong>避免DataURL</strong></p> <p>DataURL是用Base64的方式,将图片变成一串文本编码放入代码的方式。这种方式有好处,因为它可以减少一次http交互的请求,对于一些体积特别小的图片,或者是动态生成的图片可以考虑使用。但在H5应用中,一般情况下,我们都是需要避免DataURL的,因为它的数据体积通常比二进制图片的格式大1/3,而且它不会被浏览器缓存,每次页面刷新都需要重新加载这部分数据。</p> <p>使用图片的替代(css3, svg, iconfont)</p> <p>CSS3和svg可以更好地使用GPU进行渲染加速,而且会避免增加图片资源导致的http请求增加。例如一些div的圆角效果,就完全可以用用css来实现。</p> <p>Iconfont,可以认为是一种矢量类型的操作字体。如果页面中有较多的操作图标,可以考虑使用iconfont来替代图片资源。</p> <p><strong>★域名/服务端部署</strong></p> <p><strong>Gzip</strong></p> <p>服务端要开启Gzip压缩。</p> <p><strong>资源缓存,长cache</strong></p> <p>合理设置资源的过期时间,尤其对一些静态的不需要改变的资源,将其缓存过期时间设置得长一些。</p> <p><strong>分域名部署(静态资源域名)</strong></p> <p>将动态资源和静态资源放置在不同的域名下,例如图片,放在自己特定的域名下。这样的好处是,静态资源请求时,不会带上动态域名中所设置的cookie头信息,从而减少http请求的大小。</p> <p><strong>减少Cookie</strong></p> <p>尽量减少Cookie头信息的大小,因为这部分数据使用的是上行流量,上行带宽更小,所以传输速度更慢,因此要尽量精简其大小。</p> <p><strong>CDN加速</strong></p> <p>部署CDN服务器,或者使用第三方的CDN加速服务,优化不同地域接入网站的带宽速度。</p> <h3><strong>★ 代码资源</strong></h3> <p><strong>Javascript, CSS合并</strong></p> <p>尽量将所有的js和css合并,减少资源请求的次数。</p> <p><strong>外联使用js, css</strong></p> <p>外联使用js和css,这样可以有效地利用缓存,避免html页面刷新后重新加载这部分代码。</p> <p><strong>压缩html, js, css</strong></p> <p>压缩代码,尤其是js和css资源,压缩后的大小可以降低至原来的1/3以下,有效节约流量。</p> <p><strong>资源的版本更新</strong></p> <p>库js、css通常不会更新,但是我们的业务js和css可能会有更新,如果命中浏览器缓存,可能会让一些新的特性不能及时展现,甚至可能导致逻辑上的冲突。</p> <p>因此对于这些js、css的资源引入,最好用版本号或者更新时间来作为后缀,这样的话,后缀不变,命中缓存;后缀改变,浏览器自动更新最新的代码。</p> <p><strong>Css位置</strong></p> <p>CSS要放到html代码的开头的head标签结束前。如果网页是动态生成的,那么在head代码完成后可以强制输出(例如php的flush()操作),这样的话,浏览器就会更快地解析出来head中的内容,开始下载css文件资源。</p> <p><strong>Js位置</strong></p> <p>Js放到前,这样的话,js的加载不会影响初始页面的渲染。</p> <h3><strong>★代码规范</strong></h3> <p><strong>避免空src</strong></p> <p>图片设置为空的src地址,在某些浏览器中可能会导致增加一个无效的http请求,因此要避免。</p> <p><strong>避免css表达式</strong></p> <p>可能会让页面多次执行计算,造成卡顿等性能问题。</p> <p><strong>避免空css规则</strong></p> <p>降低css渲染计算的成本</p> <p>避免直接设置元素style</p> <p>直接设置style属性,一方面在html代码中不利于缓存,另一方面也不利于样式的复用,因此要避免,通过指定id或者class的方式,在css代码块中进行样式调整。</p> <h3><strong>★ 服务端接口</strong></h3> <p><strong>接口合并</strong></p> <p>如果页面需要请求两部分以上的数据接口,建议将其合并,否则会增加一次http请求。</p> <p><strong>减少接口数据量</strong></p> <p>有的时候,服务端会把一些无关紧要的数据返回回来,尤其是类似于更新时间、状态等信息,如果在客户端不影响内容的逻辑展示,不妨在接口返回的数据中直接去掉这些内容。</p> <p><strong>缓存</strong></p> <p>缓存接口数据,在一些数据新旧敏感性不高的场景下很有作用,在非首次加载数据时候优先使用上次请求来的缓存数据,可以让页面更加快速地渲染出来,而不用等待一个新的http请求结束之后再渲染。这一点我们在后面还会再次提及。</p> <h3><strong>★ 其他一些建议</strong></h3> <ul> <li> <p>合理使用css</p> <ul> <li> <p>正确使用Display属性 Display属性会影响页面的渲染,因此请合理使用</p> </li> <li> <p>display:inline后不应该再使用width、height、margin、padding以及float</p> </li> <li> <p>display:inline-block后不应该再使用float</p> </li> <li> <p>display:block后不应该再使用vertical-align</p> </li> <li> <p>display:table-*后不应该再使用margin或者float</p> </li> <li> <p>不滥用float</p> </li> <li> <p>不声明过多的font-size</p> </li> <li> <p>值为0时不需要单位</p> </li> <li> <p>标准化各种浏览器前缀</p> </li> <li> <p>无前缀应放在最后</p> </li> <li> <p>CSS动画只用 (-webkit- 无前缀)两种即可</p> </li> <li> <p>其它前缀为 -webkit- -moz- -ms- 无前缀 四种,(-o-Opera浏览器改用blink内核,所以淘汰)</p> </li> </ul> </li> <li> <p>选择器</p> <ul> <li> <p>避免让选择符看起来像是正则表达式。高级选择器不容易读懂,执行耗时也长</p> </li> <li> <p>尽量使用ID选择器</p> </li> <li> <p>尽量使用css3动画</p> </li> </ul> </li> <li> <p>资源加载</p> <ul> <li> <p>使用srcset</p> </li> <li> <p>首次加载不超过1024KB(或者可以说是越小越好)</p> </li> </ul> </li> <li> <p>html和js</p> <ul> <li> <p>减少重绘和回流</p> </li> <li> <p>缓存dom选择和计算</p> </li> <li> <p>缓存列表.length</p> </li> <li> <p>尽量使用事件代理,避免批量绑定事件</p> </li> <li> <p>使用touchstart,touchend代替click</p> </li> <li> <p>Html使用viewport</p> </li> <li> <p>减少dom节点</p> </li> <li> <p>合理使用requestAnimationFrame动画代替setTimeOut</p> </li> <li> <p>适当使用Canvas动画</p> </li> <li> <p>TouchMove, Scroll事件会导致多次渲染</p> </li> </ul> </li> </ul> <h3><strong>★ 更快一步</strong></h3> <p>前面的很多建议与普通的PC端web网页的开发是一致的,但是在移动互联网应用下,仅仅做到这些,可能只有60分,那么怎样才能得到80分甚至更高?</p> <p><strong>单页应用</strong></p> <p>钉钉的审批微应用,使用的就是单页架构。在这种架构下,基本不存在页面跳转的等待时间,只需要执行js逻辑触发界面变化,最多进行一次网络请求,获得服务端数据,其他资源均不需要再次请求。</p> <p><strong>资源离线</strong></p> <p>再快的网络交互,毕竟也是跨越了数个网络节点,因此一张图片、一个js,优化到了极致,也照样可能需要几百毫秒的时间来获得。因此想要打破这个极限,就要使用资源离线的策略。</p> <p>在钉钉的微应用中,就使用了这样的一个“离线包”策略。一些固定的图片、js库等,被打包放入app中(或根据需要,在app启动的时候进行下载更新)。</p> <p>微应用中,网页代码里面加载网络资源的需求,就变成了直接加载本地文件,速度自然得到再一次巨大的提升。</p> <p>本地数据持久化和更新机制(版本管理)</p> <p>对于一些时效性没有那么高的数据,可以考虑将接口数据缓存。那么页面的渲染将变成这样的流程:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/ebd5a7a5e3a1a2e07c070480efd78084.jpg"></p> <p>而非首次进入界面,流程如下:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/549f1af8ecc25eaae3aaf1977ed01ab2.jpg"></p> <p>可以看出,在非首次进入界面的时候,页面不需要等待网络数据返回,就可以进行界面渲染,渲染的初始数据来自于本地的缓存,页面可以“秒开”。而当服务端的数据返回之后,本地的渲染会再次更新,缓存也被更新。</p> <p>采用这样的方案有利有弊,好处显而易见,首屏加载的速度简直太快了——静态资源来自本地,数据接口来自本地,这在2G、3G或者其他网络速度较慢的时候,也可以让用户在极短的时间内就看到内容。但是这种方案也并非万能。</p> <ol> <li> <p>首次加载不可避免,还是会请求网络。</p> </li> <li> <p>服务端有更新的时候,客户端不能够快速感知,页面可能还停留在一个“旧的版本”上,尤其是网络速度较慢时,可能还是需要经过好几秒,页面才会更新至最新版本。因此如果应用对数据的新旧很敏感的话,这种方案就不适合</p> </li> <li> <p>数据更新后,需要重新渲染界面,界面刷新的性能消耗比正常情况更多,而且增加了程序的复杂度,容易出错。</p> </li> </ol> <p><strong>预加载</strong></p> <p>有时,我们能够通过用户的行为统计,预判出用户下一步可能进行的操作。假设,我们统计出来针对某个微应用,用户首页渲染完成之后,大部分会点击列表中的第一个项目查看详情。那么在首页渲染完成之后,我们就可以先预先加载第一个项目的部分内容,那么针对这部分用户,他们实际点击之后,立即就能看到新的页面中的内容。</p> <p>当然,这个方式也并不是在所有场景下都使用。一方面,需要做好充分的用户调研,掌握用户的使用习惯;另一方面,对于小部分用户而言,预加载所带来的就是不必要的流量消耗。</p> <h2><strong>经典案例</strong></h2> <h3><strong>★ 图片未优化</strong></h3> <p>通过charles可以方便地进行测试。 从请求监控的情况看,有一张图片超过了60KB,宽度640px,但是在应用中,实际显示的是一张小缩略图,是通过代码控制让它显示成小图的,因此修改方案很简单,将所有头像的图片均改为获取120px宽度的即可。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/f2b9b76fdd3ff2534e50f01848cade7a.jpg"></p> <h3><strong>★按需加载</strong></h3> <ul> <li> <p>钉钉的教学页面</p> <ul> <li> <p>多个slide页面, 每个页面有2-3个图片, 其中有一个是大图显示</p> </li> <li> <p>图片是客户提供的, 最大的图片大约是300KB以上</p> </li> <li> <p>第一次进入页面后, 所有slide的图片均进行加载</p> </li> <li> <p>3G网络下, loading的图标大约持续6000ms后才会消失</p> </li> </ul> </li> <li> <p>优化方案</p> <ul> <li> <p>尽可能优化图片, 但是压缩后发现噪点明显增加, 影响了显示效果</p> </li> <li> <p>修改加载方案, 第一次进入后, 只加载本页的图片</p> </li> <li> <p>loading时间降低至1秒左右</p> </li> </ul> </li> </ul> <p> </p> <p> </p> <p>来自:http://mp.weixin.qq.com/s/r-D4S94XOo22PQM_wZlrig</p> <p> </p>