Recently, we have been doing performance optimization for our console. This time, we have recorded the performance check and optimization of code execution (the optimization on pure JS does not include DOM operation and other optimization). Other optimizations will be shared later.

Console address: console.ucloud.cn/

Troubleshooting and collecting performance problems

First need to check out the need to optimize the point, this we can use Chrome DevTool to check performance problems in the website.

It is best to gather information in stealth mode and avoid the influence of some mods.

Performance

The first way to collect information is through the Performance panel. Expand the Main panel to see information about how your code is running. However, the Performance panel contains a lot of content, including rendering, network, memory and other information, which causes serious visual interference. Although very powerful but do pure JS performance check is not recommended to use, today mainly introduces another way.

JavaScript Profiler

Another way to do this is with JavaScript profilers, which are hidden by default and need to be opened in the More button (three-point button) in the upper right corner of DevTool => More Tools.

You can see that the JavaScript Profiler panel is much simpler than the Performance panel, with the top left row of buttons to collect, delete, garbage collect (possibly for GC enforcement, I’m not sure), and collect multiple profilers for comparison.

On the right is the display area of Profiler, and the display modes can be switched from Chart, Heavy and Tree. Chart is recommended here, which is the most intuitive and easy to understand.

The graph at the top of the Chart panel shows CPU utilization on the vertical axis, time on the horizontal axis and call stack depth on the vertical axis. The following is the time segment information of code execution. A long time segment will cause obvious lag in the page, which needs to be checked.

In Chart panel, scroll up and down to zoom in and out of the graph, scroll left and right to scroll the timeline, and you can also select and drag the mouse circle in the Chart. CMD + F can be searched, which is convenient when you want to find the performance of the corresponding code.

The JavaScript Profiler panel is a great way to identify code that is performing poorly.

For example, N. botstrap in the figure, whose execution time is 354.3ms, will obviously cause serious lag.

It can be seen from the above that l.I nitState takes 173ms, and several forEach are below. It is obvious that the loop performance consumption here is relatively high. Clicking the time fragment will jump to the corresponding code in the Source panel. It’s very easy to check.

With the help of JavaScript profilers, we can sort out all the old code that might have performance problems and put it on a to-do list for further review.

console.time

Using Profiler to sort out problem code is convenient, but in the actual tuning process, it is a bit troublesome, because each debugging needs to perform a collection, after the collection, we still need to find the current debugging point, which will waste a lot of time, so we will choose other methods in the actual tuning process. For example, calculate the timestamp difference and log it out, but there is a more convenient way – console.time.

const doSomething = (a)= > {
    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');
Copy the code

Console. time Is supported by most browsers. You can easily print the execution time of a code through console.time.

  • Console. time receives a parameter identifier and starts a timer, which can then be used to execute timeLog and timeEnd
  • TimeLog receives 1-N parameters. The first parameter is the timer identifier and the following parameters are optional parameters. After the execution, the error time of the current timer is printed and other optional parameters are passed in
  • TimeEnd is similar to timeLog except that it does not accept extra optional parameters and closes the timer after execution
  • You cannot enable multiple timers with the same identity at the same time
  • After a timer ends, you can start another timer with the same name

Through console.time, we can visually see the execution time of a piece of code. Every time the page is refreshed after a change, we can see the log and see the impact of the change.

Collating and optimizing performance issues

With the help of JavaScript Profiler, multiple performance optimization points are extracted from the console. (The following time is local debugging with DevTool enabled, higher than the actual situation)

The name of the location A single time Number of first executions Switch execution times
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

Troubleshoot performance problems according to the listed troubleshooting points. Some typical problem points are listed below.

Break up the tasks in the loop

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');
};
Copy the code

In initState, filterDatas is a route map with 1000 keys. $stateProvider. However, the two files can be deferred, and the file list can be retrieved when the file is pulled.

self.initState = function (data, common) {
    console.time('initState');
    // some code
    // Add a route to 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']));
Copy the code

By reducing the tasks in the iteration, initState was 30% to 40% faster.

Clear logic

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;
}
Copy the code

As you can see from getUserRights, bitMaps are traversed once every time, and the bitMaps themselves don’t change at all, so you just need to do the traversal once at initialization, or cache it after the first traversal.

var _bitMaps = {
    // map info
};
var bitMaps = {};
_.each(_bitMaps, function(value, key) {
    bitMaps[key.toUpperCase()] = value;
});

function getUserRights(bits, key) {
    key = (key || ' ').toUpperCase();
    returnbitMaps[key] ! =null? !!!!! +bits.charAt(bitMaps[key]) :false;
}
Copy the code

The efficiency of getUserRights is increased by 90+% with these changes, and since getUserRights is called multiple times in many of the above performance issues, this change can result in a significant performance improvement.

Bitwise operation

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
};
Copy the code

BuildRegionBitMaps combines two 512-bit permission bit binary strings to calculate the actual permissions. The current code splits the binary string into an array and iterates to calculate the permissions for each bit, which is less efficient. BuildRegionBitMaps are called multiple times in initRegionsHash, magnifying the performance problem here.

You can use bit operations to easily calculate permissions, which is much more efficient than array traversal.

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);
};
Copy the code

With the above changes, the initRegionHash running time is optimized to 2ms-8ms, an increase of 90+%. Note that JavaScript’s bitwise operations are based on 32 bits and overflow over 32 bits, so the above is broken down into 30-bit strings for merging.

Reduce repetitive tasks

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]);
}
Copy the code

The above code is used to merge product languages. Products is the product name corresponding to the route. There will be duplication.

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');
}
Copy the code

Here, the product name in the product is deleted to reduce the number of merge, and then the language merge corresponding to common and Cname is removed from the traversal, and the merge is performed at the end to reduce the number of merge and reduce the amount of data merged in the early stage. ExtendTrans speed increased by 70+% after modification.

Out as soon as possible

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;
};
Copy the code

Modules in getNav are routes. As mentioned above, there are nearly thousands of routes, and getUseRights is called in this traversal, resulting in a serious performance loss, and another very serious problem is that most of the data will be filtered by 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;
};
Copy the code

By making judgments earlier, ending meaningless code earlier, and previous optimizations to getUserRights, getNav improved its speed by 99%.

Use lazy

renderMenuList = (a)= > {
    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>
            );
        });
};
Copy the code

The above code is in a menu edit panel on the console, which only appears when the user clicks edit, but the existing logic causes this data to be frequent, filterTopNavShow will be executed 7 times on the page, and it will be re-rendered.

renderMenuList = (a)= > {
    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;
};
Copy the code

We simply add a _lazyRender field to defer rendering and computation until we first open it, avoiding unnecessary operations during page initialization.

results

Let’s take a look at the time before and after

The name of the A single time The optimization effect
initState 200ms – 400ms 120ms-300ms, reduced by 30%-40%
initRegionHash 50ms – 110ms 2ms – 8ms, 90% reduction
getMenu 0 – 40ms 0ms – 8ms, 80% reduction
initRegion 70ms – 200ms 3ms to 10ms, 90% reduction
getProducts 40ms – 80ms 3ms to 10ms, 90% reduction
getNav 40ms – 200ms 0ms-2ms, 99% reduction
extendProductTrans 40ms – 120ms 70% reduction from 10ms to 40ms
filterStorageMenu 4ms – 10ms 0ms-2ms, 80% reduction
filterTopNavShow 0 – 20ms The initial load is no longer executed

The comparison is quite obvious, and most of the time is controlled within 10ms.

Look again at the Profiler before and after the modification.

Before modification:

After optimization, we can see that many peaks have disappeared (the rest are some optimization points that are not easy to make at present), and we can also feel the difference when entering the page and switching products.

conclusion

From the above optimization code you can see, most of the performance problems are caused by circulation, a small performance issues after after many cycles may cause serious impact, so the code at ordinary times when a lot of things still need to pay attention to as much as possible, such as the end of the code as soon as possible will end as soon as possible, it is not necessary to the operation of the omit anything, to do do cache cache, By maintaining good programming habits, you can keep your code running at a good speed even in unknown situations.

With JavaScript Profiler and console.time, performance troubleshooting and optimization can be made as simple as possible, identifying problems and making it easy to optimize for them.