28

通天塔前端性能优化实践

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzUyMDAxMjQ3Ng%3D%3D&%3Bmid=2247492229&%3Bidx=1&%3Bsn=4398b7b00d090cf27b0ea9a725e4b697
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

目标提升前端和Node中间层性能

通天塔是京东内部的一个快速搭建活动页面的平台,用户可以通过在可视化平台上选择需要的模板及配置对应数据,来生成对应的原生、H5及PC活动页面。模板样式丰富,操作灵活,在京东被大量使用,用户流量也呈现出了非常迅猛的增长。但随着项目的迭代,功能越来越复杂,模板越来越多,前端和Node中间层性能问题也逐渐暴露出来,其中,前端首屏加载时间TP75性能要大于2秒,而Node中间层单核QPS相较其他应用也较低。为了提升用户体验,同时提高系统的吞吐率,在2019年下半年我们通过对现有通天塔H5项目进行一次全盘分析,做了一次全方位的性能优化。

综合性能提升超过30%

通天塔H5是以React SSR为基础进行架构的,首屏页面在Node中间层进行数据请求及渲染,分页和其他异步请求在客户端请求网关接口并渲染,静态资源托管在CDN,如图1所示。

RRnymq7.png!web

图1:通天塔H5请求流程

基于以上架构,经过2019年下半年的优化,通天塔H5前端首屏加载性能和Node中间层渲染性能都得到了极大提升。

1

首屏加载性能

首屏加载耗时,TP75从原来的2.47秒减小到了1.58秒,性能提升约36.03%。图2所示为优化前后以周为维度的首屏TP75加载时长。

fmYNbaU.png!web

图2:首屏TP75加载时间

此处首屏加载时长,指用户打开页面到首屏第一张图片请求完成所经历的时间。横坐标代表第几周,01代表今年第一周(2020/01/06 ~ 2020/01/12),52代表去年最后一周(2019/12/23 ~ 2019/12/29)。

2

服务器性能

通过对服务器进行压测,在相同QPS维度下,CPU从原来的29.52%降低至20.5%,CPU使用率相比之前降低了30.5%

vYvIZv2.jpg!web

图3:同QPS下CPU利用率

线上性能分析

在进行优化前,需要先采集当前线上数据,了解服务目前的性能情况,我们主要通过以下方式对项目的性能进行分析及监控。

1

Performance API

Performance API是ECMAScript5才引入的,精度可达到1毫秒的千分之一,目前主流浏览器基本都已经支持performance对象。通过performance.timing对象,可以拿到浏览器处理网页各个阶段的耗时。通过performance.getEntries方法,可以获取js, css, 图片及ajax在内的所有请求的耗时信息。

我们基于Performance API,封装了一个前端测速模块,该模块在页面加载完成后将所需性能数据上报至服务器,之后可以在可视化平台上进行数据的展示及分析。

2

Chrome Devtools

Chrome Devtools是前端调试及性能分析常用的工具,通过该工具,可以查看页面资源加载情况,所加载CSS、JS及图片的大小,还可以通过Performance面板,查看页面渲染绘制和Script执行情况。

3

v8-profiler

通天塔H5是基于React SSR架构的,页面首屏在Node中间层请求数据并渲染,所以除纯前端的监控及分析,还需对Node层进行性能分析及优化。在Node层性能分析中,我们主要通过v8-profiler模块进行性能分析。

在本地或测试环境下新增两个路由:

const profiler = require('v8-profiler');



router.get('/profiler/start', (req, res) => {

//Start Profiling

profiler.startProfiling('CPU profile');

res.end('profile start');

});



router.get('profiler/end', (req, res) => {

const profile = profiler.stopProfiling()

profile.export()

.pipe(res)

.on('finish', () => {

profile.delete();

res.end();

});

});

通过ab压测工具,对服务器发起请求

ab -c 10 -n 1000 http://localhost:7001/mall/active/xxx/index.html

在压测过程中,可以通过http://localhost:7001/profiler/start开始性能统计,通过http://localhost:7001/profiler/end结束性能统计,并将结果保存为 ***.cpuprofile文件,通过Chrome Devtools中的JavaScript Profiler工具进行分析。

nqABbqQ.png!web

图4:Node服务端代码执行情况

常规优化+业务特性优化

我们主要从两个方面进行性能优化,一方面是基于较为通用的前端常规性能优化方案,另一方面基于通天塔业务特点进行的偏业务方面的优化。

1

前端常规优化

在优化之初,根据《高性能网站建设指南》提及的常规优化方案,我们检查了项目中需要改进或深度优化的地方,主要涉及以下方面:

  • 尽可能的减少HTTP的请求数,减小HTTP请求大小

  • 将静态资源放在CDN,最大化利用CDN缓存能力

  • 减少CSS和JS请求个数,减小CSS和JS包大小

  • 启用gzip压缩

1 )减少图片请求大小

在通天塔页面中,图片请求一般占比最大,在前期开发过程中,针对图片请求我们已做过懒加载优化,图片请求数很难更进一步减少,但针对图片大小,我们可以进行优化。

InEV3un.png!web

图5:图片展示尺寸及实际尺寸

如图5,在页面中图片实际所展示坑位大小为115x115,即使在3倍屏上,所需图片尺寸也只是345x345,但实际请求中,图片的原始尺寸却是800x800,这对用户流量是一种浪费,同时也增加了图片加载耗时,而通天塔活动页中,这种类似的图片还有很多,而同时,京东图片服务器正好支持图片的裁剪,原来一张800x800的图片,可以按比例缩小到所需的高度,如一张800x800的原图 https://m.360buyimg.com/babel/jfs/t1/85209/24/15512/218570/5e7179d8E957c16c1/5fbcc42fad37fe94.jpg!q70.dpg ,通过修改URL,可以改成下发230x230尺寸的图 https://m.360buyimg.com/babel/s230x230_jfs/t1/85209/24/15512/218570/5e7179d8E957c16c1/5fbcc42fad37fe94.jpg!q70.dpg

鉴于此,针对图片大小,可以做按实际展示大小请求对应尺寸的图片的优化,以减小HTTP的大小。

const isJfsRegex = /360buyimg\.com\/.*\/((s([\d^_]+)x([\d^_]+)_)?jfs)/i;

export function resizeImg(url, rect) {

const dpr = window.devicePixelRatio;

if (!isJpegRegExp.test(url)) {

return url;

}

const result = url.match(isJfsRegex);

if (!result) {

return url;

}

if (result[3] && result[4]) {

if (!rect || (!rect.width && !rect.height)) {

return url;

}

if (rect.width && !rect.height) {

rect.height = rect.width / result[3] * result[4];

}

if (rect.height && !rect.width) {

rect.width = rect.height / result[4] * result[3];

}

const t = 's' + Math.ceil(rect.width* dpr) + 'x' + Math.ceil(rect.height* dpr) + '_jfs';

return url.replace(result[1], t);

} else {

if (rect && rect.width && rect.height) {

return url.replace('/jfs/', `/s${Math.ceil(rect.width * dpr)}x${Math.ceil(rect.height * dpr)}_jfs/`);

}

}

return url;

}

2 )最大化利用 CDN 缓存

在做性能优化前,通天塔静态资源的打包,是开发者在上线前,在自己电脑上进行的,且文件名会依据文件内容重新生成,格式为[filename].[contenthash:8].js。

按这种方式在个人电脑上打包,即使有package-lock.json锁定包版本,但由于个人电脑操作系统及使用的npm包管理工具的不同(有的包管理工具不读package-lock.json),node_module下的文件可能会不一致,导致文件的contenthash不同。

针对这个问题,我们基于Jenkins搭建了一个前端CI打包系统,后继所有上线前的前端静态资源打包,都迁移到CI上进行,通过这种方式,确保了文件名的一致性,以最大程度的利用CDN缓存。

3 )调整 webpack 打包策略,按需加载 CSS JS

在性能优化前,通天塔的CSS和JS资源是按以下策略打包的:

  • vendor.[contenthash:8].js: 包含node_module下的代码

  • common.[contenthash:8].js: 包含非node_modules下的代码

  • [channel].[contenthash:8].js: 通天塔有很多渠道,每个渠道的专属代码打包到这个JS中

  • template.[contenthash:8].css: 包含所有渠道通用CSS

  • [channel].[contenthash:8].css: 包含渠道专有CSS

按这种方式来进行打包,有以下两个问题,

1. 每个活动会加载所有模板对应的CSS和JS,造成不必要的加载。

2. 所有的系统模板代码都打包到common包,导致common包非常庞大,而其中有任何一个模板代码有改动,都会影响到common包的文件名,从而导致CDN缓存失效,客户端必须重新请求CDN。

针对两个问题,我们改进了打包策略,最终方案改为:

  • vendor.[contenthash:8].js: 包含node_module下的JS文件

  • lowUsedTemp.[contenthash:8].js: 包含使用频率低的系统模板代码,页面会按照活动是否使用到低频模板按需请求

  • mute.[contenthash:8].js: 包含剔除低频使用模板后,较为稳定,很少改动的系统模板

  • template.[contenthash:8].js: 包含剩余非node_modules下的代码

  • [channel].[contenthash:8].js: 包含渠道专属代码

  • lowUsedTemp.[contenthash:8].css: 包含使用频率低的系统模板代码的CSS,页面会按照活动是否使用到低频模板按需请求

  • template.[contenthash:8].css: 包含所有渠道通用CSS

  • [channel].[contenthash:8].css: 包含渠道专有CSS

按照这种方式打包,只有使用到低频使用模板的活动,才会加载lowUsedTemp.[contenthash:8].css和lowUsedTemp.[contenthash:8].js,其中按需加载的CSS占总CSS大小的25%,按需加载的JS占总大小的17%。而单独打包出的mute.[contenthash:8].js这个JS资源,由于里面包含的模板很少被改动,所以在打包上线时,其文件名也很少会变,这就可以利用CDN缓存,不会每次上线后,用户都重新请求这部分代码。

2

业务优化

在常规的前端性能优化达到瓶颈后,我们开始尝试基于业务进行性能优化。

1 )首屏精准化优化

通天塔页面是运营在可视化配置平台中,通过选择模板,配置数据来动态生成的,而其中类似商品楼层这种素材楼层,配置的素材数量也由运营自己决定,少的可能只有几个,多的几十上百个,这便导致通天塔首屏页面有以下特点

1. 页面灵活多变,页面结构难以预测。

2. 在请求首屏楼层数据时,服务端难以计算需要下发几个楼层刚好满首屏,故按照素材楼层数来进行分页,如果首页素材楼层配置的素材较多,节点数会非常庞大。

由于以上两个特点,导致很多活动页首屏的内容,远大于客户端首屏实际所需展示的长度,这既加大了首屏的渲染耗时,同时也浪费了Node服务器的CPU资源(渲染了不必要的楼层)。

另外,在通过v8-profiler测试Node服务器性能时,我们发现Node服务器端开销最大的地方有三处

  • 通过JSON.parse解析后端下发的活动数据

  • React.renderToString 进行首屏渲染

  • JSON.stringify将首屏数据序列化后跟随HTML下发给客户端

综合考虑各种优化方式,最终决定采用在Node层计算每个楼层高度,按首屏高度渲染楼层数的方案。

fIfMne2.png!web

图6:首屏精准渲染流程

1. 用户向Node中间层发起请求的时候,客户端会向cookie中埋入设备宽高信息。

2. Node中间层从cookie中获取设备宽高信息,若获取失败,则使用默认值。

3. 循环计算每个楼层的高度,如果楼层累计高度超过2倍设备高度,丢弃后面的楼层数据,并重置分页信息。

4. Node中间层根据计算过后的数据,渲染首页楼层,并将数据序列化后随HTNL下发到客户端。

5. 前端检测页面是否满两屏,若没满两屏,立即请求下一页数据。

在楼层维度的分页优化完毕后,还可以精益求精,针对素材楼层进行楼层内素材的优化,如果首页最后一个素材楼层中有素材超过两屏,还可以将超过两屏的素材降级到客户端来渲染。

上面结论中所提到的服务端性能优化,主要就是通过首屏精准化优化实现的。

2 )首屏图片预加载

由于通天塔页面的灵活性,开发者并不知道哪些楼层元素会出现在首屏,所以页面中所有的图片资源,统一设置为懒加载模式。而基于通天塔H5 SSR的架构,首页在服务端渲染完成后下发给前端,前端只有在加载完JS后判断图片是否在首屏,在首屏的才开始加载图片,这就会造成图片必须要在JS加载执行完成后才进行加载,图片坑位的白屏时间较长。

基于以上图片加载滞后的问题,前期我们做了第一个优化是,在第一屏楼层之后插入一段内联JS,获取页面已加载的图片元素,如果在首屏则先将首屏图片的data-src置为src进行加载,但发现以下问题,下面是一个测试Demo。

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Document</title>

</head>

<body>

<img data-src="/img-size.png" id="test" />

<h1>我是测试代码</h1>

<script>

(function() {

var start = Date.now();

console.log('start', start)

var img = document.getElementById('test');

img.onload = function() {

console.log('end', Date.now(), Date.now() - start); // end - start > 2000ms

}

img.src = img.getAttribute('data-src');

})();

</script>

<h1>我是测试代码22222</h1>

<img src="/performance-cpu.jpg" />

<script src="/test.js"></script>

</body>

</html>

其中test.js中很简单

console.log('start');

var start = Date.now();

for(;;) {

if (Date.now() - start > 2000) {

break;

}

}

console.log('end');

zamqIfQ.png!web

图7:Demo页加载执行

从图中可以看出这个图片依然在JS执行完毕后才加载。但我们发现performance-cpu.jpg这张图片,由于浏览器对图片资源预加载的缘故,没被JS阻塞,基于浏览器对图片的预加载,及上面做的首屏精准化渲染,我们又进行了一个优化:在计算首屏楼层高度时,给处于首屏的部分图片元素打标,根据这个标识在render渲染时,对打标的图片不进行懒加载处理,而直接用src,通过这个优化,图片将不被JS阻塞,提前了首屏图片开始加载的时机,减少了图片坑位的白屏时间。

3 )交互优化

在通天塔活动中,有许多包含多Tab的模板,如商品类模板,在之前的开发中,每次切换Tab我们都是销毁之前Tab下的内容,重新渲染新Tab下的内容,这样在重新切换回去的时候,还是需要重新渲染,造成页面的卡顿。基于此,我们将Tab渲染改成了增量渲染,这样在切回上一个Tab的时候,白屏及渲染时间会大大减少。

优化是持续性工作

在通天塔H5前端性能优化的过程中,很大一部分是站在巨人的肩膀上进行的,但光这些远远不够,当纯技术优化到一定程度后,更需要根据自己的业务特点,从业务层面进行优化,这个优化的效果可能更好。

通天塔前端的优化在此告一段落,但优化之路还远未结束,后续我们还会更进一步,基于自身的业务以及一些新技术,继续深入优化,同时也希望本文能给前端及后台开发人员带来一些新的想法。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK