Может ли производительность JSON.parse() быть улучшена?
JSON.parse - это медленный способ создания копии объекта. Но можем ли мы на самом деле улучшить производительность нашего кода?
Проблема
Создание копии объекта является обычной практикой в JS. Вы, вероятно, сделали это при создании редукторов в Redux или где-либо еще. В настоящее время наиболее используемый синтаксис для этого распространен:
const objA = { name: 'Jack', surname: 'Sparrow' };
const objB = { ...objA };
Использование на практике:
function dataReducer(state = { name: '', surname: '' }, action) {
switch (action.type) {
case ACTION_TYPES.SET_NAME:
return {
...state,
name: action.name,
};
case ACTION_TYPES.SET_SURNAME:
return {
...state,
surname: action.surname,
};
default:
return state;
}
}
Но это можно сделать разными способами (не считая разных библиотек)
const objC = Object.assign({}, objA);
const objD = JSON.parse(JSON.stringify(objA));
Если вы проверите, сколько времени потребуется, чтобы скопировать объект, используя эти методы, вы получите результаты, подобные 1⁰⁹ (каждый раз, когда мы копируем objA
):
test with spread: 14 ms.
test with Object.assign: 36 ms.
test with JSON.parse: 702 ms.
Очевидно JSON.parse
, самый медленный из них, и с некоторым отрывом. Почему вы даже должны рассмотреть возможность использования JSON.parse
вместо распространения (...
)?
Не очень очевидное поведение движка V8
Все сводится к тому, как V8 оптимизирует функции. Каждый раз, когда функция выполняется, V8 сравнивает переданный ей объект IC
(Inline Cache), и если Shape
объект хранится внутри одного из «кэшей», тогда V8 может следовать «быстрому пути».
Так что если у вас есть такая функция
function test(obj) {
let result = '';
for (let i = 0; i < N; i += 1) {
result += obj.a + obj.b;
}
return result;
}
Вы можете запустить его с несколькими объектами одинаковой формы и иметь отличную производительность
const jack = { name: 'Jack', surname: 'Sparrow' };
const frodo = { name: 'Frodo', surname: 'Baggins' };
const charles = { name: 'Charles', surname: 'Xavier' };
test(jack);
test(frodo);
test(charles);
Причина в том, что V8 собирается пометить эту функцию как мономорфную и оптимизировать ее код. Как вы знаете, только время это происходит, когда функция вызывается с одной и только одной формой.
Давайте проверим, какие формы создаются при использовании каждого из 3 методов копирования:
//testSpread.js
const objA = { name: 'Jack', surname: 'Sparrow' };
const objB = { ...objA };
console.log(%HaveSameMap(objA, objB)); // false
//testAssign.js
const objA = { name: 'Jack', surname: 'Sparrow' };
const objC = Object.assign({}, objA);
console.log(%HaveSameMap(objA, objC)); // false
//testParse.js
const objA = { name: 'Jack', surname: 'Sparrow' };
const objD = JSON.parse(JSON.stringify(objA));
console.log(%HaveSameMap(objA, objD)); // true
Как видите, JSON.parse(JSON.stringify(objA))
создайте только объект, который имеет ту же форму, что и objA
.
Стоимость немономорфных функций
Вот наша функция
function test(obj) {
let result = '';
// Any costly operation
for (let i = 0; i < N; i += 1) {
result += obj.name + obj.surname;
}
return result;
}
Здесь важно то, что функция делает дорогой и блокирует потоки. Этот пример глуп, но представьте, что там происходит какая-то сложная математическая операция. Вот как мы называем нашу функцию двумя разными способами.
const jack = { name: 'Jack', surname: 'Sparrow' };
const frodo = { name: 'Frodo', surname: 'Baggins' };
const charles = { name: 'Charles', surname: 'Xavier' };
const legolas = { name: 'Legolas', surname: 'Thranduilion' };
const indiana = { name: 'Indiana', surname: 'Jones' };
for (let i = 0; i < N; i += 1) {
test(JSON.parse(JSON.stringify(jack)));
test(JSON.parse(JSON.stringify(frodo)));
test(JSON.parse(JSON.stringify(charles)));
test(JSON.parse(JSON.stringify(legolas)));
test(JSON.parse(JSON.stringify(indiana)));
}
for (let i = 0; i < N; i += 1) {
test({ ...jack });
test({ ...frodo });
test({ ...charles });
test({ ...legolas });
test({ ...indiana });
}
Это довольно распространенный сценарий. Мы не хотим влиять на существующие объекты, поэтому мы решили создать его копию.
Если вы установите N
на 10000 и запустить этот цикл, то результат может вас удивить:
test with PARSE: 2522 ms.
test with spread: 10046 ms.
Какая? spread в 4 раза медленнее, чем JSON.parse
? Если это странно, вспомни, что я говорил раньше
V8 собирается пометить эту функцию как мономорфную и оптимизировать ее код
Поскольку тест не простая функция (код простой, но дорогой в выполнении), начальная стоимость вызова JSON.stringify
и JSON.parse
намного ниже, чем запуск этой функции без оптимизации.
Во втором тестовом прогоне эта функция становится мегаморфной, и V8 прекращает ее оптимизацию. Вы можете проверить этот репозиторий, чтобы попробовать ее на своей машине.
Вывод
Важно понимать, как работает оптимизация JS при разработке сложных методов вычисления в JavaScript. Иногда даже простая вещь может привести к снижению производительности, и вы можете потратить дни, пытаясь выяснить, что происходит.
Я не говорю о замене всех операторов распространения на JSON.parse
, это снизит производительность вашего приложения. Я хочу сказать, что иногда снижение производительности одной вещи может значительно улучшить производительность другой.
Случай, который я только что описал, является действительно особенным и может повлиять на вас только тогда, когда функция слишком дорогая, но знание этого может помочь вам по-другому подойти к проблеме.