Недавно мы оптимизировали производительность нашей консоли, в этот раз мы запишем исследование производительности и оптимизацию выполнения кода (чистый JS не включает такие оптимизации, как операции DOM). Другими моментами оптимизации есть возможность поделиться позже.
Адрес консоли:console.ucloud.cn/
Устранение неполадок производительности и сбор данных
Прежде всего, нам нужно найти точки, которые необходимо оптимизировать.Мы можем использовать Chrome DevTool для устранения проблем с производительностью на веб-сайте.
Лучше всего собирать информацию в режиме инкогнито, чтобы избежать влияния некоторых плагинов.
Performance
Первый способ — использовать панель «Производительность» для сбора информации, развернуть панель «Главная», и вы сможете увидеть информацию о работающем коде. Однако на панели «Производительность» много контента, а также другая информация, такая как рендеринг, сеть, память и т. Д., И визуальные помехи более серьезны. Несмотря на то, что он очень мощный, он не рекомендуется для устранения неполадок с производительностью только JS.Сегодня мы в основном представляем другой метод.
JavaScript Profiler
Другой способ — использовать JavaScript Profiler, который по умолчанию скрыт и его нужно открывать с помощью кнопки «Дополнительно» (кнопка с тремя точками) в правом верхнем углу DevTool => Дополнительные инструменты.
Вы можете видеть, что панель JavaScript Profiler намного проще, чем панель «Производительность». несколько раз Сравнение.
Справа находится область отображения Профилировщика. Режим отображения можно переключать выше, включая три режима: Диаграмма, Тяжелый и Дерево. Здесь рекомендуется Диаграмма, которая является наиболее интуитивно понятной и простой для понимания.
Верхняя часть панели диаграммы — это диаграмма, вертикальная ось — это загрузка ЦП, горизонтальная ось — ось времени, а вертикальная ось — глубина стека вызовов. Ниже приведена информация о временном сегменте выполнения кода. Более длительный временной сегмент вызовет явные зависания страницы, что необходимо проверить.
На панели «Диаграмма» прокрутка вверх и вниз будет увеличивать и уменьшать масштаб графика, прокрутка влево и вправо — это прокрутка временной шкалы, и вы также можете обводить и перетаскивать диаграмму с помощью мыши. CMD + f можно искать, что удобно, когда вы хотите найти соответствующую производительность кода.
С помощью панели JavaScript Profiler вы можете устранять неполадки кода с ненормальной производительностью во многих аспектах.
Например, для n.bootstrap на рисунке время выполнения составляет 354,3 мс, что, очевидно, вызовет серьезное зависание.
Вы также можете следить за временным отрезком, чтобы узнать, какой шаг занимает много времени.Из приведенного выше видно, что l.initState занимает 173 мс, а следующие - несколько forEach.Очевидно, что производительность цикла здесь относительно дорогая, и нажатие отрезок времени перейдет к соответствующему коду в исходной панели, что очень удобно проверять.
С помощью JavaScript Profiler мы можем отсортировать весь код, который занимает много времени и может иметь проблемы с производительностью, внести его в список дел и дождаться дальнейшего изучения.
console.time
Разбирать проблемный код с помощью Profiler очень удобно, но немного хлопотно в самом процессе настройки, т.к. при каждой отладке нужно производить сбор, а после сбора нужно найти текущую точку отладки , что незаметно потратит много времени, поэтому собственно настройка В процессе оптимизации мы выберем другие методы, например вычисление разницы временных меток и последующее ее логирование, но есть на самом деле более удобный метод — 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 принимает идентификатор параметра и запускает таймер, который затем можно использовать для выполнения timeLog и timeEnd с использованием идентификатора таймера.
- timeLog получает 1-n параметров, первый — это идентификатор таймера, а следующие — необязательные параметры.После выполнения он напечатает разницу времени текущего таймера и другие переданные необязательные параметры.
- timeEnd похож на timeLog, за исключением того, что он не принимает избыточные необязательные параметры и закрывает таймер после выполнения.
- Несколько таймеров с одним и тем же идентификатором не могут быть включены одновременно
- После окончания таймера вы можете снова запустить таймер с тем же именем.
Через console.time мы можем визуально увидеть время выполнения фрагмента кода.После каждого изменения страница обновляется, чтобы увидеть журнал, чтобы увидеть влияние изменения.
Сортировка и оптимизация проблем с производительностью
Используйте 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. Для инициализации вам необходимо зарегистрировать информацию о маршрутизации в ui-router. Невозможно опустить $stateProvider.state, но два файла могут быть отложены и обработаны при извлечении файлов. , Затем перейдите, чтобы получить список файлов.
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 вы можете видеть, что битмапы будут проходиться каждый раз, а сами битмапы не будут меняться, так что на самом деле вам нужно только сделать обход во время инициализации или хорошо поработать с кэшированием после начального обхода.
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-битных (см. текущий код, длина не обязательно фиксированная) двоичных строк битов разрешений для вычисления фактических разрешений. авторитет одного бита менее эффективен. BuildRegionBitMaps вызывается несколько раз в initRegionsHash, что усугубляет здесь проблему с производительностью.
Здесь можно использовать битовые операции для простого расчета разрешений, и эффективность будет намного выше, чем у обхода массива.
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 оптимизировано до 2–8 мс, что составляет увеличение более чем на 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]);
}
Приведенный выше код используется для слияния языков продуктов.В продуктах будут повторяться названия продуктов, соответствующие маршрутам.Среди них общий язык больше и имеет более 1 Вт ключей, поэтому время, необходимое для слияния, более серьезное.
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');
}
Здесь имена продуктов в продукте дедуплицируются, чтобы уменьшить количество слияний, а затем языковые слияния, соответствующие 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 — это маршруты, как упоминалось выше, маршрутов около тысячи, и здесь при обходе вызывается 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;
};
GetNav работает на 99 % быстрее благодаря опережающей оценке, раннему завершению бесполезного кода и предыдущей оптимизации для getUserRights.
хорошо использовать ленивый
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>
);
});
};
Приведенный выше код находится в панели редактирования меню консоли. Эта панель появляется только тогда, когда пользователь нажимает для редактирования, но существующая логика приводит к тому, что этот фрагмент данных появляется часто. После входа на страницу filterTopNavShow будет выполняться 7 раз. , и он будет перерисован.
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 | 120 мс - 300 мс, уменьшено на 30% -40% |
| initRegionHash | 50ms - 110ms | 2 мс - 8 мс, уменьшено на 90% |
| getMenu | 0 - 40ms | 0 мс - 8 мс, снижение на 80 % |
| initRegion | 70ms - 200ms | 3 мс - 10 мс, снижение на 90 % |
| getProducts | 40ms - 80ms | 3 мс - 10 мс, снижение на 90 % |
| getNav | 40ms - 200ms | 0 мс - 2 мс, снижение на 99 % |
| extendProductTrans | 40ms - 120ms | 10 мс - 40 мс 70% снижение |
| filterStorageMenu | 4ms - 10ms | 0 мс - 2 мс, снижение на 80 % |
| filterTopNavShow | 0 - 20ms | Первая загрузка больше не выполняется, расширьте выполнение |
Сравнение все еще относительно очевидно, большую часть времени контролируется в пределах 10 мс.
Вы можете взглянуть на график Profiler до и после преобразования.
Трансформация прежнего:
После оптимизации видно, что многие пики исчезли (остались какие-то моменты оптимизации, которые в настоящее время сделать не просто), и разницу хорошо чувствуется при входе на страницу и переключении товаров.
Суммировать
Как видно из приведенного выше кода оптимизации, большинство проблем с производительностью вызвано циклами, и небольшая проблема с производительностью будет иметь серьезное влияние после многих циклов, поэтому в обычном коде все еще нужно много вещей. возможно, например, код, который может завершиться как можно раньше, завершится как можно раньше, а все ненужные операции будут опущены.Кэшируйте те, которые следует кэшировать, и сохраняйте хорошие привычки программирования, чтобы ваш код мог быть гарантированно хорошо работать даже при неизвестных обстоятельствах скорость.
С помощью JavaScript Profiler и console.time можно очень просто устранить неполадки и оптимизировать производительность.После выявления проблемы можно легко составить план оптимизации для этой проблемы.