JavaScript 代码性能优化 - 从排查处处理

近期在对咱们的控制台作性能优化,此次记录下代码执行方面的性能排查与优化(纯 JS 上的不包含 DOM 操做等优化)。其它的优化点之后有机会再分享。编程

控制台地址:console.ucloud.cn/json

性能问题排查与收集

首先须要排查出须要优化的点,这个咱们能够借助 Chrome 的 DevTool 来排查网站中的性能问题。bootstrap

最好在隐身模式下收集信息,避免一些插件的影响。数组

Performance

第一种方式能够借助 Performance 面板来采集信息,展开 Main 面板,能够看到代码运行的信息。不过 Performance 面板中内容较多,还包含了渲染、网络、内存等其它的信息,视觉干扰比较严重。虽然很强大可是作纯 JS 性能排查时不推荐使用,今天主要介绍另外一种方式。浏览器

JavaScript Profiler

还有一种方式是借助 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,咱们能够将全部时间较长、可能有性能问题的代码所有整理出来,放到代办列表中,等待进一步排查。

console.time

借助 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 接收一个参数标识并开启一个 timer,随后可以使用这个 timer 的标识来执行 timeLog 和 timeEnd
  • timeLog 接收 1-n 个参数,第一个为 timer 标识,其后的为可选参数,执行后会打印出当前 timer 的差时,以及传入的其它可选参数
  • timeEnd 和 timeLog 相似,不一样的是不会接受多余可选参数并会在执行后关闭这个 timer
  • 不能同时启用多个一样标识的 timer
  • 一个 timer 结束后,能够再次开启一个同名 timer

经过 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%。

善用 lazy

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,性能排查和优化能够作到很是简单,排查到问题点,很容易针对问题去作优化方案。

相关文章
相关标签/搜索