近期在對咱們的控制檯作性能優化,此次記錄下代碼執行方面的性能排查與優化(純 JS 上的不包含 DOM 操做等優化)。其它的優化點之後有機會再分享。編程
控制檯地址:console.ucloud.cn/json
首先須要排查出須要優化的點,這個咱們能夠藉助 Chrome 的 DevTool 來排查網站中的性能問題。bootstrap
最好在隱身模式下收集信息,避免一些插件的影響。數組
第一種方式能夠藉助 Performance 面板來採集信息,展開 Main 面板,能夠看到代碼運行的信息。不過 Performance 面板中內容較多,還包含了渲染、網絡、內存等其它的信息,視覺干擾比較嚴重。雖然很強大可是作純 JS 性能排查時不推薦使用,今天主要介紹另外一種方式。瀏覽器
還有一種方式是藉助 JavaScript Profiler,JavaScript Profiler 默認是隱藏的,須要在 DevTool 右上角的更多按鈕(三個點的按鈕) => More tools 中打開。緩存
能夠看到 JavaScript Profiler 面板較 Performance 面板比起來簡單多了,左側最上方一排按鈕能夠收集、刪除、垃圾回收(多是用來強制執行 GC 的,不太肯定),能夠收集屢次 Profiler 進行比對。性能優化
右側是 Profiler 的展現區域,上方能夠切換展現模式,包括 Chart、Heavy、Tree 三種模式,這裏推薦 Chart,最直觀,也是最易懂的。網絡
Chart 面板上方爲圖表,縱軸爲 CPU 的使用率,高峯點是排查的重點區域。下方爲代碼執行的時間片斷信息,長度較長的時間片斷會在頁面中形成明顯的卡頓,須要重點排查。app
在 Chart 面板中,上下滾動會將圖形進行放大縮小,左右滾動爲滾動時間軸,也能夠在圖表中進行鼠標圈選和拖動。CMD + f 能夠進行搜索,在想要查找對應代碼性能的時候比較方便。dom
經過 JavaScript Profiler 面板能夠很方面的排查出性能異常的代碼。
好比圖中的 n.bootstrap,執行時間爲 354.3ms,顯然會形成比較嚴重的卡頓。
還能夠順着時間片斷往下深究究竟是哪一個步驟耗時較長,從上面能夠看到其中 l.initState 耗時 173ms,下面是幾個 forEach,顯然是這裏的循環性能消耗比較大,點擊時間片斷會跳轉到 source 面板的對應代碼中,排查起來很是方便。
藉助 JavaScript Profiler,咱們能夠將全部時間較長、可能有性能問題的代碼所有整理出來,放到代辦列表中,等待進一步排查。
藉助 Profiler 進行問題代碼整理很方便,可是在實際調優過程當中卻有點麻煩,由於每次調試都須要執行一次收集,收集完了還須要找到當前調試的點,無形中會浪費不少時間,因此實際調優過程當中咱們會選擇其餘的方式,好比計算出時間戳差值而後 log 出來,不過其實有更方便的方式 - console.time。
const doSomething = () => {
return new Array((Math.random() * 100000) | 0).fill(null).map((v, i) => {
return i * i;
});
};
// start a time log
console.time('time log name');
doSomething();
// log time
console.timeLog('time log name', 1);
doSomething();
// log time
console.timeLog('time log name', 2);
doSomething();
// log time and end timer
console.timeEnd('time log name', 'end');
複製代碼
console.time 目前大部分瀏覽器已經支持,經過 console.time 能夠很方便的打印出一段代碼的執行時間。
經過 console.time 咱們能夠直觀的看到一段代碼的執行時長,每次改動後頁面刷新就能看到 log,從而看到改動後的影響。
藉助 JavaScript Profiler,從控制檯中排查出多處性能優化點。(如下時間爲本地調試並開着 DevTool 時的數據,比實際狀況較高)
名稱 | 位置 | 單次耗時 | 首次執行次數 | 切換執行次數 |
---|---|---|---|---|
initState | route.extend.js:148 | 200ms - 400ms | 1 | 0 |
initRegionHash | s_region.js:217 | 50ms - 110ms | 1 | 0 |
getMenu | s_top_menu.js:53 | 0 - 40ms | 4 | 3 |
initRegion | s_region.js:105QuickMenuWrapper/index.jsx:72 | 70ms - 200ms | 1 | 0 |
getProducts | s_globalAction.js:73 | 40ms - 80ms | 1 | 2 |
getNav | s_userinfo:58 | 40ms - 200ms | 2 | 0 |
extendProductTrans | s_translateLoader.js:114 | 40ms - 120ms | 1 | 1 |
filterStorageMenu | QuickMenu.jsx:198 | 4ms - 10ms | 1 | 0 |
filterTopNavShow | EditPanel.jsx:224 | 0 - 20ms | 7 | 3 |
根據列出的排查的點,具體排除性能問題。下面列一些比較典型的問題點。
var localeFilesHandle = function (files) {
var result = [];
var reg = /[^\/\\\:\*\"\<\>\|\?\.]+(?=\.json)/;
_.each(files, function (file, i) {
// some code
});
return result;
};
var loadFilesHandle = function (files) {
var result = [];
var reg = /[^\/\\\:\*\"\<\>\|\?\.]+(?=\.json)/;
_.each(files, function (file, i) {
// some code
});
return result;
};
self.initState = function (data, common) {
console.time('initState');
// some code
_.each(filterDatas, function (state, name) {
var route = _.extend({}, common, state);
var loadFiles = loadFilesHandle(route['files']);
var localeFiles = localeFilesHandle(route['files']);
route['loadfiles'] = _.union(( route['common_files'] || [] ), loadFiles);
route['localeFiles'] = localeFiles;
routes[name] = route;
$stateProvider.state(name, route);
});
// some code
console.timeEnd('initState');
};
複製代碼
initState 中,filterDatas 爲一個近 1000 個 key 的路由 map,初始化是須要去 ui-router 中註冊路由信息,$stateProvider.state 是沒辦法省略了,可是 兩個 files 能夠延後化處理,在拉取文件時再去獲取文件列表。
self.initState = function (data, common) {
console.time('initState');
// some code
//添加路由到state
_.each(filterDatas, function (state, name) {
var route = _.extend({}, common, state);
routes[name] = route;
$stateProvider.state(name, route);
});
// some code
console.timeEnd('initState');
};
// when load files
!toState.loadfiles &&
(toState.loadfiles = _.union(
toState['common_files'] || [],
$UStateExtend.loadFilesHandle(toState['files'])
));
!toState.localeFiles && (toState.localeFiles = $UStateExtend.localeFilesHandle(toState['files']));
複製代碼
通過減小迭代中的任務,initState 速度提高了 30% - 40%。
var bitMaps = {
// map info
};
function getUserRights(bits,key){
var map = {};
_.each(bitMaps,function(val,key){
map[key.toUpperCase ()] = val;
});
return (map && map[(key||'').toUpperCase ()] != null) ? !!(+bits.charAt(map[(key||'').toUpperCase ()])) : false;
}
複製代碼
getUserRights 中能夠看到每次都會去對 bitMaps 作一次遍歷,而 bitMaps 自己不會有任何變化,因此這裏其實只須要在初始化時作一次遍歷就能夠了,或者在初次遍歷後作好緩存。
var _bitMaps = {
// map info
};
var bitMaps = {};
_.each(_bitMaps, function(value, key) {
bitMaps[key.toUpperCase()] = value;
});
function getUserRights(bits, key) {
key = (key || '').toUpperCase();
return bitMaps[key] != null ? !!+bits.charAt(bitMaps[key]) : false;
}
複製代碼
通過上述改動,getUserRights 的效率提高了 90+%,而上述不少性能問題點中都屢次調用了 getUserRights,因此這點改動就能帶來明顯的性能提高。
var buildRegionBitMaps = function(bit,rBit){
var result;
if( !bit || !rBit){
return '';
}
var zoneBit = (bit + '').split('');
var regionBit = (rBit + '').split('');
var forList = zoneBit.length > regionBit.length ? zoneBit : regionBit;
var diffList = zoneBit.length > regionBit.length ? regionBit : zoneBit;
var resultList = [];
_.each(forList,function(v,i){
resultList.push(parseInt(v) || parseInt(diffList[i] || 0));
});
result = resultList.join('');
return result;
};
var initRegionsHash = function(data){
// some code
_.each(data,function(o){
if(!regionsHash[o['Region']]){
regionsHash[o['Region']] = [];
regionsHash['regionBits'][o['Region']] = o['BitMaps'];
regionsList.push(o['Region']);
}
regionsHash['regionBits'][o['Region']] = buildRegionBitMaps(o['BitMaps'],regionsHash['regionBits'][o['Region']]);
regionsHash[o['Region']].push(o);
});
// some code
};
複製代碼
buildRegionBitMaps 是將兩個 512 位長(看當前代碼,長度未必固定)的權限位二進制字符串進行合併,計算出實際的權限,目前的代碼將二進制字符串拆解爲數組,而後遍歷去計算出每一位的權限,效率較低。initRegionsHash 中會調用屢次 buildRegionBitMaps,致使這裏的性能問題被放大。
這裏可使用位運算來方便的計算出權限,效率會比數組遍歷高不少。
var buildRegionBitMaps = function(bit, rBit) {
if (!bit || !rBit) {
return '';
}
var result = '';
var longBit, shortBit, shortBitLength;
if (bit.length > rBit.length) {
longBit = bit;
shortBit = rBit;
} else {
longBit = rBit;
shortBit = bit;
}
shortBitLength = shortBit.length;
var i = 0;
var limit = 30;
var remainder = shortBitLength % 30;
var mergeLength = shortBitLength - remainder;
var mergeString = (s, e) =>
(parseInt('1' + longBit.substring(s, e), 2) | parseInt('1' + shortBit.substring(s, e), 2))
.toString(2)
.substring(1);
for (; i < mergeLength; ) {
var n = i + limit;
result += mergeString(i, n);
i = n;
}
if (remainder) {
result += mergeString(mergeLength, shortBitLength);
}
return result + longBit.slice(shortBitLength);
};
複製代碼
經過上述改動,initRegionHash 運行時間被優化到 2ms - 8ms,提高 90+%。注意 JavaScript 中位運算基於 32 位,超過 32 位溢出,因此上面拆解爲 30 位的字符串進行合併。
function () {
currentTrans = {};
angular.forEach(products, function (product, index) {
setLoaded(product['name'],options.key,true);
currentTrans = extendProduct(product['name'],options.key, CNlan);
});
currentTrans = extendProduct(Loader.cname||'common',options.key, CNlan);
if($rootScope.reviseTrans){
currentTrans = Loader.changeTrans($rootScope.reviseNoticeSet,currentTrans);
}
deferred.resolve(currentTrans[options.key]);
}
複製代碼
上述代碼被用來進行產品語言的合併,products 中是路由對應的產品名,會有重複,其中 common 的語言較大,有 1W 多個 key,因此合併時耗時較爲嚴重。
function () {
console.time('extendTrans');
currentTrans = {};
var productNameList = _.union(_.map(products, product => product.name));
var cname = Loader.cname || 'common';
angular.forEach(productNameList, function(productName, index) {
setLoaded(productName, options.key, true);
if (productName === cname || productName === 'common') return;
extendProduct(productName, options.key, CNlan);
});
extendProduct('common', options.key, CNlan);
cname !== 'common' && extendProduct(cname, options.key, CNlan);
if ($rootScope.reviseTrans) {
currentTrans = Loader.changeTrans($rootScope.reviseNoticeSet, currentTrans);
}
deferred.resolve(currentTrans[options.key]);
console.timeEnd('extendTrans');
}
複製代碼
這邊將 product 中的產品名去重減小合併次數,而後將 common 和 cname 對應的語言合併從遍歷中剔除,在最後作合併來減小合併次數,減小前期合併的數據量。 通過改動後 extendTrans 速度提升了 70+%。
user.getNav = function(){
var result = [];
if ( _.isEmpty ( $rootScope.USER ) ) {
return result;
}
_.each ( modules , function ( list ) {
var show = true;
if ( list.isAdmin === true ) {
show = $rootScope.USER.Admin == 1;
}
var authBitKey = list.bitKey ? regionService.getUserRights ( list.bitKey.toUpperCase () ) : show;
var item = _.extend ( {} , list , {
show : show,
authBitKey : authBitKey
} );
if ( item.isUserNav === true ) {
result.push ( item )
}
} );
return result;
};
複製代碼
getNav 中的 modules 爲路由,上面也提到過,路由較多有近千,而在這裏的遍歷中調用了 getUseRights,致使性能損失嚴重,而且又一個很是嚴重的問題是,大部分的數據會被 isUserNav 篩除掉。
user.getNav = function(){
var result = [];
if ( _.isEmpty ( $rootScope.USER ) ) {
return result;
}
console.time(`getNav`);
_.each ( modules , function ( list ) {
if(list.isUserNav !== true) return;
var show = true;
if ( list.isAdmin === true ) {
show = $rootScope.USER.Admin == 1;
}
var authBitKey = list.bitKey ? regionService.getUserRights ( list.bitKey.toUpperCase () ) : show;
var item = _.extend ( {} , list , {
show : show,
authBitKey : authBitKey
} );
result.push ( item );
} );
console.timeEnd(`getNav`);
return result;
};
複製代碼
經過將判斷提早,儘早結束無心義的代碼,和以前對 getUserRights 所作的優化,getNav 的速度提升了 99%。
renderMenuList = () => {
const { translateLoadingSuccess, topMenu } = this.props;
if (!translateLoadingSuccess) {
return null;
}
return topMenu
.filter(item => {
const filterTopNavShow = this.$filter('filterTopNavShow')(item);
return filterTopNavShow > 0;
})
.map((item = [], i) => {
const title = `INDEX_TOP_${(item[0] || {}).type}`.toUpperCase();
return (
<div className="uc-nav__edit-panel-item" key={i}> <div className="uc-nav__edit-panel-item-title"> {formatMessage({ id: title })} </div> <div className="uc-nav__edit-panel-item-content"> <Row gutter={12}>{this.renderMenuProdList(item)}</Row> </div> </div>
);
});
};
複製代碼
上述代碼在控制檯的一個菜單編輯面板中,這個面板只有用戶點擊了編輯纔會出現,可是現有邏輯致使這塊數據會常常,一進頁面會執行 7 次 filterTopNavShow,而且還會從新渲染。
renderMenuList = () => {
const { translateLoadingSuccess, topMenu, mode } = this.props;
if (!translateLoadingSuccess) {
return null;
}
if (mode !== 'edit' && this._lazyRender) return null;
this._lazyRender = false;
const menuList = topMenu
.filter(item => {
const filterTopNavShow = this.$filter('filterTopNavShow')(item);
return filterTopNavShow > 0;
})
.map((item = [], i) => {
const title = `INDEX_TOP_${(item[0] || {}).type}`.toUpperCase();
return (
<div className="uc-nav__edit-panel-item" key={i}> <div className="uc-nav__edit-panel-item-title"> {formatMessage({ id: title })} </div> <div className="uc-nav__edit-panel-item-content"> <Row gutter={12}>{this.renderMenuProdList(item)}</Row> </div> </div>
);
});
return menuList;
};
複製代碼
這邊簡單的經過添加一個 _lazyRender 字段,將渲染和計算延遲到初次打開時再去作,避免了頁面初始化時的沒必要要操做。
先看下改造先後的時間對比
名稱 | 單次耗時 | 優化效果 |
---|---|---|
initState | 200ms - 400ms | 120ms - 300ms,減小 30%-40% |
initRegionHash | 50ms - 110ms | 2ms - 8ms,減小 90% |
getMenu | 0 - 40ms | 0ms - 8ms,減小 80% |
initRegion | 70ms - 200ms | 3ms - 10ms,減小 90% |
getProducts | 40ms - 80ms | 3ms - 10ms,減小 90% |
getNav | 40ms - 200ms | 0ms - 2ms,減小 99% |
extendProductTrans | 40ms - 120ms | 10ms - 40ms 減小 70% |
filterStorageMenu | 4ms - 10ms | 0ms - 2ms,減小 80% |
filterTopNavShow | 0 - 20ms | 初次加載再也不執行,展開執行 |
對比仍是比較明顯的,大部分時間都控制在了 10ms 之內。
能夠再看一下改造先後的 Profiler 的圖形。
改造前:
改造後:通過優化能夠看到不少峯值都已經消失了(剩餘的是一些目前不太好作的優化點),進入頁面和切換產品時也能明顯感覺到差別。
從上述優化代碼中能夠看到,大部分的性能問題都是由循環帶來的,一個小小的性能問題在通過屢次循環後也會帶來嚴重的影響,因此平時代碼時不少東西仍是須要儘量注意,好比能儘快結束的代碼就儘快結束,沒有必要的操做一律省略,該作緩存的作緩存,保持良好的編程習慣,可讓本身的代碼哪怕在未知狀況下也能保證良好的運行速度。
藉助 JavaScript Profiler 和 console.time,性能排查和優化能夠作到很是簡單,排查到問題點,很容易針對問題去作優化方案。