有货移动WEB端性能优化探索实践

Posted 有货技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了有货移动WEB端性能优化探索实践相关的知识,希望对你有一定的参考价值。

在移动互联网的时代里,对于一个web站点来说,移动端的用户体验尤为重要。现代web站点的设计和开发都是以移动优先作为第一原则,我们也专门为了移动端的web站点做了相应的优化和提升。而网页的打开速度和页面的流畅度,对于用户是否长时间访问至关重要。我们在移动端的站点通过一系列的方法,最终为了快速打开页面展示网页内容,触达用户,同时能流畅的浏览网页。

移动端的硬件条件,网络条件相对于桌面端,会复杂的多,设备类型多样,硬件配置参差不齐,分辨率碎片化,网络状况在移动过程中稳定性,速率都会变化,而对于一个页面到达用户的终端展示,会经过,用户发起请求,服务端接受请求,服务端处理请求,返回响应内容,在用户终端的浏览器展示内容,用户操作页面发起其他页面时间,而这个过程中任何一个环节的延迟都会造成性能瓶颈,降低用户继续访问的可能性,所以我们在服务器端,浏览器端,网络加载,多个方面做了一系列的优化工作。

WEB服务端优化

有货的WEB端主要使用了nodejs,基于后端服务提供的HTTP接口服务来实现的前后端分离,这里的服务端优化主要是指在nodejs实现的web服务端进行优化。

                          

 

优化的目的是提升服务端的响应和并发能力,充分发挥nodejs的异步非阻塞的特性,主要从以下几个方面去优化。 

接口服务调用的优化

对于一个页面展示的路由,要处理这个路由,可能需要调用多个接口并且进行进行界面逻辑的处理。大体过程:有货移动WEB端性能优化探索实践

·       接口合并 我们对于一个页面调用可以合并的接口,进行接口合并,减少接口调用次数,如:以商品详情页为例,商品的一些特性,可以在一个接口返回,尽可能的减少接口调用的个数,因为每次接口的处理都有网络IO,对象序列化,压缩和解压的过程。

·       接口异步调用 但是并不是所有的接口都可以合并,对于无法合并的接口,我们尽量使用node的异步非阻塞的特性,进行异步调用,同时调取多个接口,而最终的调用耗时取决于最慢的接口。

这里要说明一点:对于接口依赖,如A接口依赖B接口的返回结果,像这种情况,我们最好梳理下接口设计,减少这样的串行调用,因为这样,调用耗时是多个接口耗时的总和。

·       减少接口交互数据 返回的数据较多的情况下,会导致JSON序列化,数据批量对象处理,产生额外的性能损耗。可以做下接口返回数据结构的精简,返回必要的字段(页面会展示用到的数据)以及可以调整返回item个数。从而达到减少数据的返回消息体的大小。此外请求接口时需要gzip压缩,可以大大的减少网络传输的时间,尽管需要解压会消耗一部分CPU的时间,但是对接网络IO的损耗,还是值得的。

可以分享一组基准压测数据,在调用接口使用gzip和不使用gzip的QPS数据

const rq = require('request-promise');
const callApi = async(isGzip,times) => {
    let index = 0;
    const timeFlag = process.hrtime(); 
    for (let i = 0; i < times; i++) {
        await rq({
            url: 'http://xxx.xxx.xxx.xxx/top250.json',//替换成内网的http接口
            gzip: isGzip
        }).then(data => {
            index++;
        });
    }
    const duration = process.hrtime(timeFlag); 
    console.log(`gzip ${isGzip}:${Math.round((duration[0] * 1e9 + duration[1]) / 10000) / 100}ms`);
    console.log(`result:${index}`); 
    Promise.resolve('done');
}; 
(async ()=>{
   await callApi(false,100);
   await callApi(true,100);
   await callApi(false,1000);
   await callApi(true,1000);
})();
gzip false:821.74ms
result:100
gzip true:581.83ms
result:100
gzip false:9168.54ms
result:1000
gzip true:5846.5ms
result:1000

·       减少接口调用次数 如何减少接口调用次数呢?对于一个页面,可能就会存在调用必要的接口,在这里我们使用到了缓存机制,对于热数据进行接口缓存,我们使用了一些内存数据库,同时对于一些规格数据可以进行进程级的缓存(如:导航信息,品类信息等)。缓存是有一定原则的:第一,需要容易命中的数据,第二,可以被缓存的数据,数据更新频率是可控的。通过缓存机制,一部分接口调用就会走到缓存,减少的接口调用的IO。此外缓存的数据可以是对接口数据处理后的视图对象,同时也减少的数据处理的时间。

·       内部服务调用DNS缓存 我们的内部服务使用域名方式,为了提高服务的灵活配置,但是需要内部DNS服务器进行域名解析,这个是有一定耗时的,所以我们在DNS这块加了DNS应用端cache,减少DNS解析的时间。


业务处理的优化

现在我们主要的服务端业务处理,主要对于页面逻辑的处理,如路由控制,会话处理,视图对象处理,模板渲染。我们在这些处理过程中进行了一些优化。

如何发现node的性能问题,主要可以使用cpu-profile进行cpu处理堆栈的抓取,然后使用chrome的dev-tools进行火焰图的分析,找到性能瓶颈。

·       计算密集型操作使用原生实现 js是不擅长计算密集型的操作,如Hash处理,加密解密,压缩解压,像这些操作可以直接使用nodejs提供的原生实现(crypto, Zlib)

以下是一组使用原生和js的md5处理性能对比:

const md5js = require('md5.js');
const crypto = require('crypto');
const testText = '中文qwerttyuuuioopp[[]ddffgasjdkaskldadasssssssssdsadacasfadsd;a';
const testMd5 = (title, text, func) => {
    let ret;
    const timeFlag = process.hrtime(); 
    for (let i = 0; i < 10000; i++) {
        ret = func(text);
    }
   
    const duration = process.hrtime(timeFlag); 
    console.log(`${title}:${Math.round((duration[0] * 1e9 + duration[1]) / 10000) / 100}`);
    console.log(`result:${ret}`);
}
testMd5('native md5:', testText, words => {
    return crypto.createHash('md5').update(words, 'utf8').digest('hex');
});
testMd5('js md5:', testText, words => {
    returnnew md5js().update(words, 'utf8').digest('hex');
});

返回结果:

native md5::31.59
result:5d3b7d53fdd4daaa2d75370e8a5d1789
js md5::181.54
result:5d3b7d53fdd4daaa2d75370e8a5d1789

差距还是比较明显的。

·       模板渲染的优化 我们在实际使用过程中,发现模板的渲染是十分消耗性能的,特别的模板的预处理过程,如果预处理过程是在用户访问过程中去处理,会慢不止一个数量级,所以我们把预处理的过程提前了(改造了hbs),在启动web应用时,已经预编译完成。同时我们发现handlebars的一些默认配置属性,如缩减处理,在字符串拼接过程中会损耗一定的性能,所以可以关闭html片段的缩减。

此外,我们还把可以缓存的html片段进行进程级的缓存,性能提升显著,可以把一些不怎么会变的html公共部分进行缓存。通过内部缓存刷新机制进行定时刷新html片段。

nginx的优化

·       启用page cache 使用了nginx的proxy_cache模块,配置了一些缓存机制,不同页面路由的缓存时长会读取node服务在http头里面返回的max-age时间。

proxy_cache cache_one_wap;
proxy_cache_valid 200 1m;
proxy_cache_min_uses 1;
proxy_cache_key $host$uri$args;
add_header X-Cache-Status $upstream_cache_status;

然后我们会在应用服务添加max-age配置的中间件,对路由进行拦截装饰http header:

const cachePage = {
    '/': x * MINUTE,
    '/boys': x * MINUTE,
    '/girls': x * MINUTE,
    '/kids': x * MINUTE,
    '/lifestyle': x * MINUTE,
    ...
}

另外要注意一个设置nginx缓存的时候,如果有服务端设置cookies的情况下,并且以服务端cookies的值作为标识用户会话信息,不要设置proxy_ignore_headers "Set-Cookie";,不然缓存会导致会话信息窜读的情况。

·       全站HTTPS 为什么要上全站https呢,这个主要考虑到https可以防止中间人攻击以及内容劫持,提高网站的访问安全性。但是因为多了SSL/TLS的服务端和浏览器端的处理,在性能方面也会有相应的下降,但是我们同时也启用HTTP/2,而主要的特性:多路复用 (Multiplexing)多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。另外PWA里面的serviceworker 也是必须要求网站的协议是https的,同时也是为了这个做了铺垫,HTTPS是现代WEB的发展趋势。至于PWA不是本文讨论的重点,可以在后续其他的主题展开。


浏览器端优化

移动终端五花八门,导致过重的浏览器的处理和效果,会导致体验的不一致,特别是安卓手机,所以我们在浏览器端的策略是,尽量轻量化网页,当前页面只处理当前必要的内容多页面的方式。这个和现在google提出的开源项目AMP是一个思路。

首屏直出优化

从用户发出请求到页面完全展示,一般来说在网络正常基本上1s以上,但是如果页面打开耗时超过1s,用户流失的概率就会线性上升。所以移动端秒开至关重要,所以我们的思路是减少白屏时间,尽快把浏览器可视区域展示出来,就是所谓的首屏,我们就从以下几个方面做了优化。

·       直出文档,简化dom结构 服务端进行HTML渲染输出,只处理首屏需要的HTML,并且简化DOM的树状结构,如:

<div>
   <ul>
      <li>xxxx</li>
      <li>xxxx</li>
   </ul>
</div>

可以简化成

   <ul>
      <li>xxxx</li>
      <li>xxxx</li>
   </ul>

在页面只保留必要的DOM,此外,可以估算下世面手机分辨率,确定最大首屏的输出可视区域的DOM,如果需要滚屏到第二屏的,可以延迟通过Ajax获取内容加载。减少DOM,可以减少HTML的输出,当然更重要的浏览器的布局和渲染的时间大大减少。当然首屏的静态资源和样式要优先加载,下个关键点就是首屏只加载所需样式和静态资源。

·       首屏只加载所需样式和静态资源 光有DOM的处理是远远不够的,要从白屏到展示完整的首屏,还需要样式和静态资源。所以要优先加载样式和静态资源,所以直接把公共样式中首屏用到的样式抽离出来,并且首屏用到的样式,直接在html页面内置。此外,图片和字体等其他需要展示的部分,优先加载,促使首页快速展示出来。

·       首屏渲染,js延迟执行 当首屏渲染的时候,这时候js的执行可能会阻塞渲染的线程,所以为了减少对浏览器主线程的渲染过程,尽量延迟进行js执行,特别是操作DOM的情况,不然首屏展示过程中会产生额外的重布局和重绘,js引入或代码直接放到页面的底部,在body之后,在html之前。

·       优化直出服务端处理时间 另外再强调下直出的关键,服务端的处理wait尽量减少,对于首屏直出至关重要。

图片优化

·       图片质量和体积控制 对于移动终端来说,分辨率相当于桌面会小很多,首先会降低图片的分辨率,以及图片的DPI值,第二步会降低图片的质量,以保证图片的体积变小。

·       提高CDN缓存命中,第一,减少缩略图的尺寸规格,第二,尽量和其他端的图片规格保持一致,如APP端,小程序。

·       WEBP的运用 webp不是所有的浏览器都支持,目前支持webp的浏览器,如下:有货移动WEB端性能优化探索实践

参见:https://caniuse.com/#feat=webp

所以,我们的做法是对于需要js加载的图片,进行webp的判断,如果支持,就是直接加载webp的格式图片,如果不支持,采用默认的jpg的格式。

if (window.supportWebp && (/format\/png/i.test(query) || /format\/jpg/i.test(query))) {
    imgUrl = imgUrl.replace(/format\/png/i, 'format/webp').replace(/format\/jpg/i, 'format/webp');
}

有货移动WEB端性能优化探索实践


浏览器端缓存优化

当存在缓存,可以减少浏览器的再次请求,大大提升了网页的打开速度,一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。那么下面我们就来看看服务器端缓存的原理。

·       缓存优化(max-age) 页面的缓存状态是由http协议的header决定的,我们主要使用了max-age(单位S),设置缓存的最长有效时间,使用的是时间长短,例如说我设置max-age=60,也就是说在请求发出后的60秒内,浏览器再次请求时不会再请求服务器,而是从浏览器缓存中读取数据。

                                                   有货移动WEB端性能优化探索实践

·       预加载和懒加载  预加载和懒加载是一对好兄弟,用的好,可以极高提升浏览器端的体验,就是要确定在何时预加载,何时懒加载。我们主要在浏览器首屏结束后,当浏览器相对idle的时候,可以预加载下一屏即将展示的内容。当用户在即将触发下一屏时,下一屏的数据或DOM已经stay by了,自然体验会流畅很多,但是在预加载是需要一个度,因为一个页面的DOM过多,对于浏览器占有的内存也会过多,预加载最好是用户即将触发需要浏览的内容,如第二屏,轮播后面的内容,tab页等。懒加载的运用场景主要还是为了减少单次DOM渲染的大小,对于当前页面的非可视区域,当需要展示或用户事件触发才进行加载。所以懒加载和预加载,在不同的场景下会有不同的运用,前提是保障页面的流畅度。

·       DNS预读取 DNS预读取配置的DNS的解析,可以减少DNS的次数,也可以加速不同域名的资源加载,目前支持的浏览器还是比较多的。有货移动WEB端性能优化探索实践


配置也很简单:

<link rel="dns-prefetch" href="//cdn.yoho.cn">
<link rel="dns-prefetch" href="//static.yohobuy.com">
<link rel="dns-prefetch" href="//img10.static.yhbimg.com">
<link rel="dns-prefetch" href="//img11.static.yhbimg.com">
<link rel="dns-prefetch" href="//img12.static.yhbimg.com">
<link rel="dns-prefetch" href="//img13.static.yhbimg.com">
<link rel="dns-prefetch" href="//analytics.m.yohobuy.com">
<link rel="dns-prefetch" href="//search.m.yohobuy.com">
<link rel="dns-prefetch" href="//list.m.yohobuy.com">
<link rel="dns-prefetch" href="//guang.m.yohobuy.com">

当然最好的减少DNS的时间是减少站点使用DNS的数量,我们会去掉部分二级域名。

CSS,JS的优化

项目构建主要采用了webpack的工具链,对css,进行依赖管理和构建打包,最小化css,js,并针对我们现有的多页面项目进行多入口的分包管理。

·       多页面css和js构建 打包代码如下,便利js的源文件目录,构建各个页面模块的js,所有的页面会包含两个js文件,libjs(全局公用的js),xxx.js(当前页面特有的js),css也是一样。这样每个页面的js和css都会最小化,同时我们也对这些个静态字符串文件进行gzip压缩,当然这些文件会按照版本进行静态存储,以及CDN的缓存。

   // 构建各模块子页面JS
    // 新的生成规则 module/page/index.js
    shelljs.ls(path.join(__dirname, '../js/**/index.js')).forEach((f) => {
        const dir = _.slice(f.split('/'), -3); // [modulename, page, index.js]
        // Important
        // 生成规则:module.page: './js/module/page/index.js'
        entries[`${dir[0]}.${dir[1]}`] = path.join(__dirname, `../js/${dir.join('/')}`);
    });

DOM优化

页面流畅度和DOM渲染和操作息息相关,渲染流程大致如下:

·       处理HTML 标记并构建 DOM 树。

·       处理 CSS 标记并构建 CSSOM 树。

·       将 DOM 与 CSSOM 合并成一个渲染树。

·       根据渲染树来布局,以计算每个节点的几何信息。

·       将各个节点绘制到屏幕上。有货移动WEB端性能优化探索实践

 

可以使用DEVTOOLS分析整个渲染过程中那块存在性能问题。

·       简化DOM,DOM操作优化 简化DOM可以减少渲染过程的时间,优化DOM操作,可以减少重布局和重绘的时间。简化DOM在上面的首屏直出已经介绍过相应的做法。这里主要说下DOM操作的优化,第一,减少DOM操作次数,可以把多次DOM操作在js的执行过程中生成好结果HTML,一次插入到DOM;第二,尽量在使用不在页面DOM树里面直接操作,可以脱离文档流的DOM中进行操作,可以使用fragment,一次插入文档流中。当然现在的react和vue都使用虚拟DOM的技术,通过diff算法进行通用化的DOM操作。这个也不失是一种效率高效的做法,但是对于一些不易优化的页面,还是需要人为干预和操作DOM使其性能最好。

·       减少重布局和重绘 第一,要减少布局调整,当您更改样式时,浏览器会检查任何更改是否需要计算布局,以及是否需要更新渲染树。对“几何属性”(如宽度、高度、左侧或顶部)的更改都需要布局计算。第二,绘制的复杂度、减小绘制区域:除 transform 或 opacity 属性之外,更改任何属性始终都会触发绘制。绘制通常是像素管道中开销最大的部分;应尽可能避免绘制。通过层的提升和动画的编排来减少绘制区域。可以使用 Chrome DevTools 来快速确定正在绘制的区域。打开 DevTools,按下键盘上的 Esc 键。在出现的面板中,转到“rendering”标签,然后选中“Show paint rectangles”。每次发生绘制时,Chrome 将让屏幕闪烁绿色。如果看到整个屏幕闪烁绿色,或看到你认为不应绘制的屏幕区域,则应当进一步研究。

                                             

  • 页面动画优化 尽量使用CSS3的动画,使用transform 和 opacity 属性更改来实现动画。使用 will-change 或 translateZ 提升移动的元素。避免过度使用提升规则;各层都需要内存和管理开销。此外,需要减少动画的图层,每多一个图层,会多一份内存占有和管理的开销。如果一定要使用js的动画,建议使用:requestAnimationFrame。此外,能不用页面动画的场景尽量不要使用动画,如果一定要使用,可以简化动画渲染的过程,之前我们使用过一个js插件,iScroll就是一个案例,页面内初始化了多个iScroll实例,特别在安卓手机上特别卡顿,最后,我们的解决办法是自己使用css3动画和touch事件简单轻量的实现需要滑动的部分,对于页面滚动部分使用了原生的scroll,保证了不同终端体验一致。

移动web端的优化以上每个点如果展开去讲,都可以单独写一篇文章,我们分别在以上方面做了优化,并且,也产生了比较不错的效果,移动端的打开速度和体验都有了不错的提升,普遍打开的时间提升了30-50%,在网络稳定的情况下,基本上服务端的耗时在50ms以内,首屏时间在500ms以内,但是优化这件事情是永无止境的,没有最好,只有更好,需要开发者探究根本勇于创新,达到更好的更优的境地。

我们后续,还可以在web的基本技术点上深挖,同时在PWA以及AMP等现代web新思维的多个方面大家积极面对,继续探索,在未来的web前端之路可以走的更好,提供更优的用户体验,创造更高社会价值。


以上是关于有货移动WEB端性能优化探索实践的主要内容,如果未能解决你的问题,请参考以下文章

关于有货的移动Web端性能优化探索实践

有货APP HTTPS优化探索和实践

FlutterWeb性能优化探索与实践

FlutterWeb性能优化探索与实践

首发:深入探索 Flutter 性能优化,再也没有比这还全的了

低延时直播技术优化实践,画质增强技术应用实践,云剪辑架构设计与演进,Web端音视频通话技术探索...