記一次Node.js直出服務的性能優化

做者:肖睦羣、李剛鬆javascript

一.問題背景

MPM(Market Page Maker)是京東社交電商部的組件化的頁面可視化搭建平臺,於2016年9月份上線,平均每週150+個頁面,目前已經成爲社交電商部的一個核心繫統。系統使用Vue.js做爲組件化的基礎框架,並於2017年5月份上線了Node.js直出服務。MPM的頁面會被運營同窗拿到各類渠道投放,總體流量很不穩定,對於流量的暴漲狀況要可以及時處理,這對於開發同窗來講是一個比較煩的工做。html

前幾天忽然收到告警信息,因爲運營同窗將某個MPM活動頁面投放了外部廣告,直出服務流量大漲,服務器CPU使用率達到了80%以上,因而立馬申請擴容,問題雖解決,可是留給了咱們一個問題:直出服務可否優化,此次量級的流量進來以後,是否能夠穩定支撐而不須要擴容?vue

二.分析方法及問題點

因爲本次告警問題主要是流量暴漲致使的CPU使用率過大,咱們本次重點優化服務的CPU消耗性能。分析CPU消耗的方法有多種,咱們選擇其中操做比較簡單的v8-profiler方案:安裝NPM包v8-profiler,在直出服務中添加監控代碼,打包發佈到預發佈環境進行壓測,收集監控數據再進行分析。監控代碼以下:java

const profiler = require('v8-profiler');
const fs = require('fs');
(function cpuProf() {
    setTimeout(function () { 
        console.log('開始收集CPU數據');
        profiler.startProfiling('CPU profile');
        setTimeout(function () { 
            const profile = profiler.stopProfiling();
            profile.export(function (err, result) {
                fs.writeFileSync('profile.json', result);
                profile.delete();
                console.log('CPU數據收集完成');
            });
        }, 1000 * 60 * 5);//監控數據採集5分鐘
    }, 1000);
})();
複製代碼

上述代碼會採集服務端5分鐘的CPU消耗數據,並生成一個JSON文件,將此文件下載到本地後,導入到在線分析網址https://www.speedscope.app/ (或者用Chrome DevTool也能夠),能夠看到火焰圖以下:node

從火焰圖能夠看到函數的調用棧,從上而下就是調用棧,其中橫條長度越長表明這佔用cpu的時間越長。若是某個橫條很長,可是下面又沒有很細小的子調用,通常就表示該調用消耗時間比較長,能夠考慮作優化。從圖中咱們能夠看到,消耗性能的主要有幾個地方: 1)replace函數 2)compile函數 3)parse函數 4)vue渲染git

爲了方便後文的分析,咱們先了解一下直出服務的處理過程:github

步驟 處理流程 資源消耗類型 備註
1 服務收到請求,解析頁面參數 CPU計算
2 從Redis中讀取頁面數據(PageData) 網絡IO PageData包括頁面的各類配置信息,如頁面頭尾模板、頁面樓層信息、身份判斷要求、組件元數據等
3 解析PageData CPU計算
4 組裝後端請求參數 CPU計算
5 發起後端請求並等待返回 網絡IO
6 解析後端接口返回的JSON數據 CPU計算
7 頁面模板構造 CPU計算 因爲存在用戶身份判斷(如某些組件僅對新人可見)、樓層BI等緣由,組件的容器是動態構造的
8 組件渲染 CPU計算 此處的組件渲染是Vue組件的服務端渲染
9 吐出頁面HTML 網絡IO

三.replace函數調用優化

分析具體的replace函數調用以前,咱們先詳細分析一下上面表格的第7步:頁面模板構造。express

1.頁面模板構造

因爲存在用戶身份判斷(如某些組件僅對新人或者VIP用戶可見)、樓層BI(每一個用戶展現的樓層順序都不同)等緣由,相同頁面對於不一樣的用戶展現的組件數量、順序都是不同(即千人千面),所以頁面的模板是基於各個組件的模板動態構造的。爲方便對組件進行操做,每一個組件都有一個div容器,容器構造很簡單,示例代碼以下:npm

<div id='com_1001'>__vue_com_1001_replace__</div>
<div id='com_1002'>__vue_com_1002_replace__</div>
<div id='com_1003'>__vue_com_1003_replace__</div>
<div id='com_1004'>__vue_com_1004_replace__</div>
複製代碼

其中__vue_com_1001_replace__這種是佔位符,須要用相應位置的組件的實際模板來替換。可是這裏有個問題, Vue渲染的時候,使用Render Function進行渲染的,並非普通的字符串模板或者Vue模板。下面是一段模板編譯後的Render Function:json

_c('commontag',{ref:"__uid__replace__str__",attrs:{"uid":"__uid__replace__str__","params":params___uid__replace__str__},inlineTemplate:{render:function(){with(this){return _c('div',[(true)?[(params.transparent != 1)?_c('div',{staticClass:"vueSeparator",style:({'background-color':params.color,  height: params.height + 'px'})}):_c('div',{staticClass:"vueSeparator",style:({height: params.height + 'px'})})]:_e()],2)}},staticRenderFns:[]}})
複製代碼

若使用的是Vue模板,則會在運行時作一次編譯,編譯爲Render Function,比較耗性能,所以官方推薦的作法是在構建時預編譯,而且運行時使用不包含編譯函數的精簡版。目前MPM每一個組件存儲到Redis中的也是Render Function,而不是原始的Vue模板。因此如今的問題是,已知子組件編譯後的Render Function,而且知道各個組件的DOM結構形式的容器,可否構造出父組件的Render Function?

答案固然是能夠:能夠經過字符串操做,構造出父組件的Render Function!

咱們如下面這段代碼爲例,看看構造過程(爲了簡單處理,咱們用了內聯模板):

<ParentComponent>
    <SubComponent1 inline-template :param="data.sub1">
        <p>this is SubComponent1{{param.name}}</>
    </SubComponent1>
    <SubComponent2 inline-template :param="data.sub2">
        <p>this is SubComponent2{{param.name}}</>
    </SubComponent2>
    <SubComponent3 inline-template :param="data.sub3">
        <p>this is SubComponent3{{param.name}}</>
    </SubComponent3>
</ParentComponent>
複製代碼

上述代碼通過Vue.compile函數編譯處理後,會獲得一個包含render和staticRenderFns兩個屬性的對象,咱們主要看render屬性,它是一個匿名函數,代碼以下:

function anonymous( ) {
with(this){return _c('ParentComponent',[_c('SubComponent1',{attrs:{"param":data.sub1},inlineTemplate:{render:function(){with(this){return _c('p',[_v("this is SubComponent1"+_s(param.name)+"\n\t")])}},staticRenderFns:[]}}),_v(" "),_c('SubComponent2',{attrs:{"param":data.sub2},inlineTemplate:{render:function(){with(this){return _c('p',[_v("this is SubComponent2"+_s(param.name)+"\n\t")])}},staticRenderFns:[]}}),_v(" "),_c('SubComponent3',{attrs:{"param":data.sub3},inlineTemplate:{render:function(){with(this){return _c('p',[_v("this is SubComponent3"+_s(param.name)+"\n\t")])}},staticRenderFns:[]}})],1)}
}
複製代碼

將上面的代碼再格式化一下:

function anonymous() {
with(this){return 
_c('ParentComponent',
[
_c('SubComponent1',{attrs:{"param":data.sub1},inlineTemplate:{render:function(){with(this){return _c('p',[_v("this is SubComponent1"+_s(param.name)+"\n\t")])}},staticRenderFns:[]}}),_v(" "),
_c('SubComponent2',{attrs:{"param":data.sub2},inlineTemplate:{render:function(){with(this){return _c('p',[_v("this is SubComponent2"+_s(param.name)+"\n\t")])}},staticRenderFns:[]}}),_v(" "),
_c('SubComponent3',{attrs:{"param":data.sub3},inlineTemplate:{render:function(){with(this){return _c('p',[_v("this is SubComponent3"+_s(param.name)+"\n\t")])}},staticRenderFns:[]}})
],1)}
}
複製代碼

能夠看到上面第四、五、6行代碼,就是子組件的Render Function,他們包裹在一個數組裏。所以,若是知道子組件的Render Function,配合形以下面的模板,就能夠反過來構造出父組件的Render Function(固然有一個從字符串到函數的反序列化過程,可是在咱們的場景這個不可避免,由於模板是從Redis中讀取出來的)。

function anonymous() {
with(this){return 
_c('ParentComponent',
[
__SubComponent1_replace__,
__SubComponent2_replace__,
__SubComponent3_replace__
],1)}
}
複製代碼

再回到咱們的問題,咱們已知子組件的Render Function,而且已知父組件的容器,須要構造出父組件的Render Function。如今思路就很清晰了,咱們只須要把開頭那段包含佔位符的div容器代碼,

<div id='com_1001'>__vue_com_1001_replace__</div>
<div id='com_1002'>__vue_com_1002_replace__</div>
<div id='com_1003'>__vue_com_1003_replace__</div>
<div id='com_1004'>__vue_com_1004_replace__</div>
複製代碼

使用Vue.compile函數將其編譯成Render Function,處理成字符串後,再經過正則替換其中的子組件的佔位符,變成子組件模板,最後反序列化爲父組件的Render Function便可。總體處理邏輯以下:

2.問題代碼分析

瞭解了上述處理過程,咱們再根據火焰圖中的調用棧,找到replace函數調用的問題代碼:

Object.keys(MPM_COM_STYLE_MAP).forEach(function(comId){
    var styleKey = MPM_COM_STYLE_MAP[comId];
    var code = '';
    if(hideComIds.indexOf(comId)!=-1){
        code = HIDE_TPL;
    }else if(loadingComs.indexOf(comId)!=-1){
        code = LOADING_TPL;
    }else if(MPM_STYLE_TPL_MAP[styleKey]) {
    	// 第一次replace替換
        code = MPM_STYLE_TPL_MAP[styleKey].replace(/__uid__replace__str__/g, comId); 
    } else{
        console.error('最終替換,發現無模板組件',comId);
    }
    if(code) {
    	//第二次replace替換
    	compileTpl = compileTpl.replace(`_v("__vue__${comId}__replace__")`,code);
	}  
});

複製代碼

能夠看到有兩次replace函數調用,第一次是組件ID替換(即uid替換),第二次是組件模板替換。

先分析第一次replace函數調用。 前面提到,每一個組件的模板已經編譯爲Render Function並存在Redis中。可是同一個組件在頁面中可能有多個實例,每一個實例須要有一個ID來區分,咱們稱爲uid(unique ID的意思),uid只有在運行的時候才生成,在編譯的時候是不知道的,所以用了一個佔位符(即下圖中的__uid__replace__str__),在直出服務中須要作替換,即上面代碼中的uid替換。下面是一段編譯後的代碼:

每一個頁面會有不少個組件(數十個甚至上百個),每次替換都是在以前替換的結果之上進行的,造成了循環替換,前面致使告警的那個頁面用到的編譯以後的模版最大的有20+KB,而每次正則替換以後的模版會愈來愈長,因此這裏耗時較多也就不奇怪了。

從邏輯上講,這段代碼是必不可少的,可是又有性能瓶頸,如何優化?

3.uid替換優化

咱們研究發現:對於比較長的字符串,先用字符串的split方法分割成數組,再用數組的join方法將切割的數組合併爲一個字符串,比正則替換的效率要高。此法咱們稱爲數組粘合法。如下爲測試代碼:

const exeCount = 10000000;   //執行次數,此處分別換成1W、10W、100W、1000W

//測試字符串,須要比較長的字符串才能看到效果,下面是從咱們的組件模板中摘取的一段
const str = `_c('ds',{ref:"__uid__replace__str__",attrs:{"uid":"__uid__replace__str__","params":params___uid__replace__str__,"tab-index":"3"},inlineTemplate:{render:function(){with(this){return _c('div',{attrs:{"stylkey":data.styleKey,"pc":data.pc,"actid":data.actid,"areaid":data.areaid}},[_c('ul',{directives:[{name:"getskuad",rawName:"v-getskuad",value:({bindObj:data, appendName:'skuAd', show: params.extend.showAds}),expression:"{bindObj:data, appendName:'skuAd', show: params.extend.showAds}"}],staticClass:"pinlei_g3col"},[(true)?_l((params.fnObj.translate(data.itemList)),function(item,index){return (!params.shownum || index < params.shownum || data.showMore)?_c('li',{class:['pinlei_g3col_col', (params.extend.imgSize == '1' ? 'size_230x230' : 'size_230x320')],attrs:{"index":index}},[_c('div',{staticClass:"pinlei_g3col_img"},[_c('a',{attrs:{"href":params.extend.buttonType == '5' ? addRd(goPingouUrl(item.sUrl),params.ptag) : addRd(item.sUrl,params.ptag)}},[_c('img',{attrs:{"init_src":getImgUrl('//img12.360buyimg.com/mcoss/'+ item.sPicturesUrl),"data-size":"230x230"}})]),((params.extend.sellOut != '0') && (item.dwStock - 0 > 0))?_c('div',{staticClass:"pinlei_g3col_msk"},[_m(0,true)]):_e()]),_c('div',{staticClass:"pinlei_g3col_info"},[_c('div',{class:['pinlei_g3col_t1', 'red', (params.extend.titleHeight == '1' ? 'oneline' : '')]},[_v("\n "+_s(item.sProductName)+"\n ")]),(!params.fnObj.isBeforeActive(params.extend.beginTime))?_c('div',{staticClass:"pinlei_g3col_price red",style:({color: params.extend.isShowTokenPrice == '1' && item.dwTokenPrice && (Number(item.dwTokenPrice) != 0)?'#888':''})},[_v("\n ¥"),_c('b',[_v(_s(item.dwRealTimePrice.split('.')[0]))]),_v("."+_s(item.dwRealTimePrice.split('.')[1])+"\n ")]):_e(),(params.fnObj.isBeforeActive(params.extend.beginTime))?_c('div',{staticClass:"pinlei_g3col_price red",style:({color: params.extend.isShowTokenPrice == '1' && item.dwTokenPrice && (Number(item.dwTokenPrice) != 0)?'#888':''})},[_v("\n ¥"),_c('b',[_v(_s(params.fnObj.getYushouInt(item, params.extend.priceType)))]),_v(_s(params.fnObj.getYushouDecimal(item, params.extend.priceType))+"\n ")]):_e(),(params.extend.isShowTokenPrice == '1')?[_c('div',{staticClass:"pinlei_g3col_token"},[(item.dwTokenPrice && (Number(item.dwTokenPrice) != 0))?_c('div',{staticClass:"pinlei_g3col_token_price"},[_v("專屬價:¥"),_c('b',[_v(_s(parseFloat(item.dwTokenPrice)))])]):_e()])]:_e(),(params.fnObj.isBeforeActive(params.extend.beginTime))?[_c('div',{staticClass:"pinlei_g3col_desc red"},[(item.sBackUpWords[0] && (params.fnObj.getYushouJiaDiff(item,params.extend.priceType) > 0))?[_v("比如今買省"+_s(params.fnObj.getYushouJiaDiff(item,params.extend.priceType))+"元")]:(item.sTag)?[_v(_s(item.sTag.split('|')[0]))]:(params.extend.showAds == '1' && item.skuAd)?[_v(_s(item.skuAd))]:_e()],2)]:_e(),(!params.fnObj.isBeforeActive(params.extend.beginTime))?[_c('div',{staticClass:"pinlei_g3col_desc red"},[(item.sTag)?[_v(_s(item.sTag.split('|')[0]))]:(params.extend.showAds == '1' && item.skuAd)?[_v(_s(item.skuAd))]:_e()],2)]:_e(),(params.fnObj.isBeforeActive(params.extend.beginTime))?[(params.extend.buttonType == '0')?[(params.extend.priceType == '1')?_c('div',{directives:[{name:"addcart",rawName:"v-addcart",value:({skuId: item.ddwSkuId}),expression:"{skuId: item.ddwSkuId}"}],class:{'pinlei_g3col_btn':true, 'blue':params.extend.beginTime, 'red':(!params.extend.beginTime), 'right': item.sBackUpWords[2]},style:(params.extend.priceType == 0?'border-radius: 24px;':'')},[_v("\n "+_s(params.extend.buttonWording)+"\n ")]):_e(),(params.extend.priceType == '0')?_c('div',{directives:[{name:"addcart",rawName:"v-addcart",value:({skuId: item.ddwSkuId}),expression:"{skuId: item.ddwSkuId}"}],class:{'pinlei_g3col_btn':true, 'blue':params.extend.beginTime, 'red':(!params.extend.beginTime), 'right': item.sBackUpWords[2]},style:(params.extend.priceType == 0?'border-radius: 24px;':'')},[_v("\n "+_s(params.extend.buttonWording)+"\n ")]):_e()]:_e(),(params.extend.buttonType == '1')?[_c('a',{attrs:{"href":addRd(item.sUrl,params.ptag)}},[_c('div',{class:{'pinlei_g3col_btn':true, 'blue':params.extend.beginTime, 'red':(!params.extend.beginTime)},style:(params.extend.priceType == 0?'border-radius: 24px;':'')},[_v("\n "+_s(params.extend.buttonWording)+"\n ")])])]:_e(),(params.extend.buttonType == '5')?[_c('a',{attrs:{"href":addRd(goPingouUrl(item.sUrl),params.ptag)}},[_c('div',{class:{'pinlei_g3col_btn':true, 'blue':params.extend.beginTime, 'red':(!params.extend.beginTime)},style:(params.extend.priceType == 0?'border-radius: 24px;':'')},[_v("\n "+_s(params.extend.buttonWording)+"\n ")])])]:_e(),(params.extend.buttonType == '2')?[_c('a',{attrs:{"href":addRd(item.sUrl,params.ptag)}},[_c('div',{class:{'pinlei_g3col_btn':true, 'blue':params.extend.beginTime, 'red':(!params.extend.beginTime)},style:(params.extend.priceType == 0?'border-radius: 24px;':'')},[_v("\n 定金"+_s(item.sBackUpWords[1].split('+')[0])+"抵"+_s(parseFloat((item.sBackUpWords[1].split('+')[1] * item.sBackUpWords[1].split('+')[0]).toFixed(2)))+"\n ")])])]:_e(),(params.extend.buttonType == '3')?[_c('div',{directives:[{name:"yuyue",rawName:"v-yuyue",value:({bindObj:data,stop:true, activeId:params.extend.yuyueID,appendTo:item,appendName:'state',msg:[]}),expression:"{bindObj:data,stop:true, activeId:params.extend.yuyueID,appendTo:item,appendName:'state',msg:[]}"}],class:['pinlei_g3col_btn','blue', item.state == 1 ? 'disabled' : ''],style:(params.extend.priceType == 0?'border-radius: 24px;':''),attrs:{"yuyueid":params.extend.yuyueID}},[_v("\n "+_s(params.extend.buttonWording)+"\n ")])]:_e(),(params.extend.buttonType == '4' )?[((params.fnObj.getYushouJiaDiff(item,params.extend.priceType)> 0))?_c('div',{directives:[{name:"skuyuyue",rawName:"v-skuyuyue",value:({bindObj:data,stop:true, skuId:item.ddwSkuId,appendTo:item,ignoreHistory:true,msg:{success: '預定成功,請留意京東JD.COM服務號的活動提醒',exist: '已設置預定,無需再進行設置',systemError: '該商品不是預定活動商品'},actPrice:params.fnObj.getYushouInt(item, params.extend.priceType)+params.fnObj.getYushouDecimal(item, params.extend.priceType),classId:item.classId1+'_'+item.classId2+'_'+item.classId3}),expression:"{bindObj:data,stop:true, skuId:item.ddwSkuId,appendTo:item,ignoreHistory:true,msg:{success: '預定成功,請留意京東JD.COM服務號的活動提醒',exist: '已設置預定,無需再進行設置',systemError: '該商品不是預定活動商品'},actPrice:params.fnObj.getYushouInt(item, params.extend.priceType)+params.fnObj.getYushouDecimal(item, params.extend.priceType),classId:item.classId1+'_'+item.classId2+'_'+item.classId3}"}],class:['pinlei_g3col_btn','blue', item.state == 1 ? 'disabled' : ''],style:(params.extend.priceType == 0?'border-radius: 24px;':'')},[_v("\n "+_s(params.extend.buttonWording)+"\n ")]):_c('div',{directives:[{name:"skuyuyue",rawName:"v-skuyuyue",value:({bindObj:data,stop:true, skuId:item.ddwSkuId,ignoreHistory:true,appendTo:item,msg:{success: '預定成功,請留意京東JD.COM服務號的活動提醒',exist: '已設置預定,無需再進行設置',systemError: '該商品不是預定活動商品'}}),expression:"{bindObj:data,stop:true, skuId:item.ddwSkuId,ignoreHistory:true,appendTo:item,msg:{success: '預定成功,請留意京東JD.COM服務號的活動提醒',exist: '已設置預定,無需再進行設置',systemError: '該商品不是預定活動商品'}}"}],class:['pinlei_g3col_btn','blue', item.state == 1 ? 'disabled' : ''],style:(params.extend.priceType == 0?'border-radius: 24px;':'')},[_v("\n "+_s(params.extend.buttonWording)+"\n ")])]:_e(),(params.extend.buttonType == '6' )?[_c('div',{directives:[{name:"yuyue",rawName:"v-yuyue",value:({bindObj:data,stop:true,noTip:true,activeId:params.extend.yuyueID,appendTo:item,appendName:'state',msg:[]}),expression:"{bindObj:data,stop:true,noTip:true,activeId:params.extend.yuyueID,appendTo:item,appendName:'state',msg:[]}"},{name:"addcart",rawName:"v-addcart",value:({skuId: {skuId: item.ddwSkuId,successTxt:'預定加車成功'}}),expression:"{skuId: {skuId: item.ddwSkuId,successTxt:'預定加車成功'}}"}],class:{'pinlei_g3col_btn':true, 'blue':params.extend.beginTime, 'red':(!params.extend.beginTime), 'left': params.fnObj.getCouponInfo(item.sBackUpWords[2])},style:(params.extend.priceType == 0?'border-radius: 24px;':''),attrs:{"yuyueid":params.extend.yuyueID}},[_v("\n "+_s(params.extend.buttonWording)+"\n ")])]:_e()]:_e(),(!params.fnObj.isBeforeActive(params.extend.beginTime))?[(params.extend.buttonActiveType == '0')?[_c('div',{directives:[{name:"addcart",rawName:"v-addcart",value:({skuId: item.ddwSkuId}),expression:"{skuId: item.ddwSkuId}"}],staticClass:"pinlei_g3col_btn",style:(params.extend.priceType == 0?'border-radius: 24px;background-color: #ea1e54;':'')},[_v("\n "+_s(params.extend.buttonActiveWording)+"\n ")])]:_e(),(params.extend.buttonActiveType == '1')?[_c('a',{attrs:{"href":addRd(item.sUrl,params.ptag)}},[_c('div',{staticClass:"pinlei_g3col_btn red",style:(params.extend.priceType == 0?'border-radius: 24px;background-color: #ea1e54;':'')},[_v("\n "+_s(params.extend.buttonActiveWording)+"\n ")])])]:_e(),(params.extend.buttonActiveType == '2')?[_c('a',{attrs:{"href":addRd(goPingouUrl(item.sUrl),params.ptag)}},[_c('div',{staticClass:"pinlei_g3col_btn red",style:(params.extend.priceType == 0?'border-radius: 24px;background-color: #ea1e54;':'')},[_v("\n "+_s(params.extend.buttonActiveWording)+"\n ")])])]:_e(),(params.extend.buttonActiveType == '4')?[_c('div',{directives:[{name:"addcart",rawName:"v-addcart",value:({skuId: item.ddwSkuId, bindObject: item, bindPropertyName: 'addCartMsg', isPullQuan: true}),expression:"{skuId: item.ddwSkuId, bindObject: item, bindPropertyName: 'addCartMsg', isPullQuan: true}"},{name:"quan",rawName:"v-quan",value:({bindObj:data,key:params.extend.key, level:params.extend.level, num:1, msg:{}, appendTo:item, appendName:'status', ignoreHistory:false, style:2, successUrl:item.successUrl, type:1, coupondes:{value: params.extend.price, gate: params.extend.gate, name: params.extend.name}}),expression:"{bindObj:data,key:params.extend.key, level:params.extend.level, num:1, msg:{}, appendTo:item, appendName:'status', ignoreHistory:false, style:2, successUrl:item.successUrl, type:1, coupondes:{value: params.extend.price, gate: params.extend.gate, name: params.extend.name}}"}],staticClass:"pinlei_g3col_btn",style:(params.extend.priceType == 0?'border-radius: 24px;background-color: #ea1e54;':'')},[_v("\n "+_s(params.extend.buttonActiveWording)+"\n ")])]:_e()]:_e()],2),(params.extend.corner != '0')?[(isRange(params.extend.cornerBegin, params.extend.cornerEnd) && params.extend.cornerDes)?_c('div',{staticClass:"pinlei_g3col_stamp red"},[_v(_s(params.extend.cornerDes))]):(item.sCopyWriting)?_c('div',{staticClass:"pinlei_g3col_stamp red"},[_v(_s(item.sCopyWriting))]):_e()]:_e()],2):_e()}):_e()],2),(params.shownum && data.itemList.length > params.shownum)?[_c('div',{class:'pinlei_more '+ (data.showMore?'pinlei_more_unfold':''),on:{"click":function($event){toggleMore($event)}}},[_v(_s(data.showMore?'收起更多':'展開更多'))])]:_e()],2)}},staticRenderFns:[function(){with(this){return _c('div',{staticClass:"pinlei_g3col_msk_ico"},[_c('div',{staticClass:"pinlei_g3col_msk_text"},[_v("\n 已搶光\n "),_c('br'),_v("over\n ")])])}}]}})`
//正則替換法start
const timeStart = new Date().getTime();
for(var i = 0; i < exeCount; i++) {
    str.replace(/__uid__replace__str__/g, 'com_1001');
}
const timeEnd = new Date().getTime();
console.log('正則替換耗時:', timeEnd - timeStart);
//正則替換法end
//數組粘合法start
const timeStart2 = new Date().getTime();
const segs = str.split('__uid__replace__str__');
for(var i = 0; i < exeCount; i++) {
    segs.join('com_1001');
}
const timeEnd2 = new Date().getTime();
console.log('數組粘貼耗時:', timeEnd2 - timeStart2);
//數組粘合法end
複製代碼

結果以下:

執行次數 正則替換法耗時(ms) 數組粘合法耗時(ms) 正則替換法耗時/數組粘合法耗時
1W 42 25 1.68
10W 362 179 2.01
100W 3555 1623 2.2
1000W 36449 18634 1.95

能夠看到數組粘合法的耗時是正則替換法的一半左右。

考慮到咱們的場景就是字符串比較大,存在循環替換,且是海量服務的場景,所以上面第一次替換,直接改爲數組粘合法便可。

4.組件模板替換優化

問題代碼中的第二次替換,是將容器裏的組件佔位符替換爲子組件的Render Function。即下圖所示:

子模板替換優化的替換次數實際上是跟組件的數量相關的,即便有150個組件,用數組粘合法也不會有明顯的性能提高,所以須要考慮別的辦法。

咱們查了一下vue-template-compiler的源碼(Vue的compile能力也是用此模塊),發現Vue.compile的函數有2個參數,第一個參數是待編譯的Vue模板,第二個參數是一個option對象,包含一個名爲tansformCode鉤子函數(參見資料https://github.com/vuejs/vue/blob/dev/flow/compiler.js#L38-L45 ,此參數並未在官網的文檔中暴露,關於此函數的用處後面能夠再寫一篇文章) ,這個鉤子函數接受兩個參數,第一個是ast節點,第二個是編譯該節點以後的render code,而該函數的返回值會就是最終的render code。因而在以前的生成dom函數那裏把com佔位符替換爲一個空的div元素,div元素的id爲以前的佔位符,而後在編譯的時候在transformCode鉤子函數這裏作一個替換,當發現ast節點爲div而且id符合組件佔位符的規則,那麼就返回該組件對應的編譯以後樣式模版。具體代碼以下:

var compileTpl = compiler.compile(`<div>${html}</div>`, {
            modules: {
                transformCode: function (el, code) {
                    if (el.attrsMap && el.attrsMap['id'] && el.attrsMap['id'].match(/__vue__com_\d{4,5}__replace__/)) {
                        var comId = el.attrsMap['id'].match(/com_\d{4,5}/)[0];
                        // console.log('--------------------------------', comId);
                        var styleTemplate  = compiledComTplMap[comId];
                        // console.log(styleTemplate);
                        return styleTemplate;
                    }
                    return code;
                }
            }
        }).staticRenderFns.toString();
複製代碼

這樣一來就徹底省去了第二次字符串替換的操做,因而組件編譯這裏的流程了下面這樣:

這兩次優化以後而後從新壓測並收集性能數據,獲得的火焰圖以下:

能夠看到createApp函數裏面原來的那個replace函數的橫條已經消失不見了,說明前面的優化是有效果的,最耗時的操做已經不是replace而是vue的compile方法即模版的編譯。今後次優化先後的服務端壓測的CPU數據也能說明問題:

四.compile函數調用優化

compile函數調用,就是前面"頁面模板構造"那一節提到的,將組件的容器模板用Vue.compile函數編譯成Render Function,雖然這段容器模板很簡單,可是他是一個很耗性能的操做。並且這是Vue自身提供的能力,彷佛已經沒有多大的優化餘地了。有沒有其餘優化方法呢?

仔細觀察一下組件容器dom以及編譯以後的代碼,彷佛是有規律的。若是組件樹的結構是下面這樣的:

[
    {id: "com_1001"},
    {
        id: "com_1002",
        child: [
            {id: "com_1003"},
            {id: "com_1004"}
        ]
    }
];
複製代碼

拼接以後的html內容大概是下面這樣的:

<div>
    <div id="com_1001_con"></div>
    <div id="com_1002_con"></div>
    <div mpm_edit_child_box tabpid="com_1002" class="childBox">
        <div id="com_1003_con"></div>
        <div id="com_1004_con"></div>
    </div>
</div>
複製代碼

這裏通常都只是一些簡單的模版,編譯出來大概是這樣的:

with(this) {
    return _c('div', [
        _c('div', {attrs: {"id": "com_1001_con"}}),
        _v(" "),
        _c('div', {attrs: {"id": "com_1002_con"}}),
        _v(" "),
        _c('div', {staticClass: "childBox", attrs: {"mpm_edit_child_box": "", "tabpid": "com_1002"}}, [
            _c('div', {attrs: {"id": "com_1003_con"}}),
            _v(" "),
            _c('div', {attrs: {"id": "com_1004_con"}})
        ])
    ])
}
複製代碼

經過觀察能夠發現,這裏都是生成的div元素,div上的屬性都是靜態屬性,由此咱們能夠本身實現一個簡單的「編譯」函數,不用走vue的編譯:

function simpleCompile(comList) {
            function genTree(tree) {
                var html = '';
                for (var i = 0, len = tree.length; i < len; i++) {
                    var node = tree[i];
                    var comId = node.id;
                    html += `_c('div',{attrs:{"id":"${comId}_con"}},[`;
                    html = html + compiledComTplMap[comId] + '])';  // compiledComTplMap[comId] 該組件對應的編譯後的樣式模版
                    if (node.child && node.child.length) {
                        html += `,_c('div',{staticClass:"childBox",attrs:{"mpm_edit_child_box":"","tabpid":"${comId}"}},[` + genTree(node.child) + `])`;
                    }
                    html += (i === len - 1)  ? '' : ',';
                }
                return html;
            }
            return genTree(comList);
        }
複製代碼

經測試,這樣簡單「編譯」以後生成的代碼跟以前編譯的代碼是同樣的,在預發佈環境測試了多個頁面以後,頁面渲染也沒有問題。去掉Vue模版編譯以後整個組件渲染的邏輯就變成了下面這樣:

Vue編譯優化以後收集cpu數據獲得的火焰圖以下:

從火焰圖能夠看出,原來的那個compile函數調用的橫條也消失了,說明優化有效果。再看看壓測的CPU消耗狀況:

須要提到的是,因爲是本身實現了一個簡單版的compile函數,前文中關於compile函數調用優化的代碼,也直接去掉了,固然也到達了優化的效果。

五.其餘優化研究

通過上面兩次優化以後,剩下最耗性能的地方是JSON解析和Vue渲染了。咱們也作了一下研究,可是很惋惜,暫時沒什麼成果,不過咱們的探索也能夠提一下:

1)JSON解析。咱們的服務從Redis中讀出來的PageData比較大,通常有100多KB,很須要有一個高性能的JSON反序列化的庫(即代替JSON.parse)。目前有一個高性能的庫fast-json-stringify,可是惋惜他是作序列化的(即作的是JSON.stringify作的事情)。咱們測試了多個方案,目前原生的JSON.parse函數性能仍然是最好的。

2)Vue渲染。有位騰訊的同窗提到,用string-based的模板代替VirtualDom的渲染方案提高性能,不過他忽略了一點,Vue是徹底的組件化的、是有生命週期鉤子、方法、計算屬性等,不是一個簡簡單單的模板引擎,按照他的思路是須要把生命週期的鉤子、方法、計算屬性等所有算好後拿到的數據對象,再跟string-based模板結合才能渲染,這個顯然是和組件化的思路背道而馳的。

上面2點,各位看官若是有好的思路,歡迎不吝賜教!

六.總結

此次優化總的來講,CPU性能消耗獲得了有效優化,總體提高了大概20%,一方面爲公司節省了資源,另一方面也減小了因流量暴漲致使咱們要擴容的概率,一箭雙鵰。


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:

WecTeam
相關文章
相關標籤/搜索