Skip to content

Commit e07960d

Browse files
author
Jacob Groß
committed
Increase perforance by a few %
- by rewriting some "map" functions to for loops - using "?." syntax instead of ||{} in find - hoisting regexes & functions
1 parent 2f19b8b commit e07960d

File tree

6 files changed

+132
-109
lines changed

6 files changed

+132
-109
lines changed

src/_makeChunks.mjs

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,63 @@
1+
const _stringRegex = /string/;
2+
3+
const _replaceString = (type) =>
4+
_stringRegex.test(type) ? '"__par__"' : "__par__";
5+
6+
const _isLastRegex = /^("}|})/;
7+
8+
// 3 possibilities after arbitrary property:
9+
// - ", => non-last string property
10+
// - , => non-last non-string property
11+
// - " => last string property
12+
const _matchStartRe = /^(\"\,|\,|\")/;
13+
114
/**
215
* @param {string} str - prepared string already validated.
316
* @param {array} queue - queue containing the property name to match
417
* (used for building dynamic regex) needed for the preparation of
518
* chunks used in different scenarios.
619
*/
7-
export default (str, queue) => str
8-
// Matching prepared properties and replacing with target with or without
9-
// double quotes.
10-
// => Avoiding unnecessary concatenation of doublequotes during serialization.
11-
.replace(/"\w+__sjs"/gm, type => (/string/.test(type) ? '"__par__"' : '__par__'))
12-
.split('__par__')
13-
.map((chunk, index, chunks) => {
20+
const _makeChunks = (str, queue) => {
21+
const chunks = str
22+
// Matching prepared properties and replacing with target with or without
23+
// double quotes.
24+
// => Avoiding unnecessary concatenation of doublequotes during serialization.
25+
.replace(/"\w+__sjs"/gm, _replaceString)
26+
.split("__par__"),
27+
result = [];
28+
29+
for (let i = 0; i < chunks.length; ++i) {
30+
const chunk = chunks[i];
31+
1432
// Using dynamic regex to ensure that only the correct property
1533
// at the end of the string it's actually selected.
1634
// => e.g. ,"a":{"a": => ,"a":{
17-
const matchProp = `("${(queue[index] || {}).name}":(\"?))$`;
18-
const matchWhenLast = `(\,?)${matchProp}`;
35+
const matchProp = `("${(queue[i] || {}).name}":(\"?))$`;
1936

2037
// Check if current chunk is the last one inside a nested property
21-
const isLast = /^("}|})/.test(chunks[index + 1] || '');
38+
const isLast = _isLastRegex.test(chunks[i + 1] || "");
2239

2340
// If the chunk is the last one the `isUndef` case should match
2441
// the preceding comma too.
25-
const matchPropRe = new RegExp(isLast ? matchWhenLast : matchProp);
26-
27-
// 3 possibilities after arbitrary property:
28-
// - ", => non-last string property
29-
// - , => non-last non-string property
30-
// - " => last string property
31-
const matchStartRe = /^(\"\,|\,|\")/;
42+
const matchPropRe = new RegExp(isLast ? `(\,?)${matchProp}` : matchProp);
3243

33-
return {
44+
result.push({
3445
// notify that the chunk preceding the current one has not
3546
// its corresponding property undefined.
3647
// => This is a V8 optimization as it's way faster writing
3748
// the value of a property than writing the entire property.
3849
flag: false,
3950
pure: chunk,
4051
// Without initial part
41-
prevUndef: chunk.replace(matchStartRe, ''),
52+
prevUndef: chunk.replace(_matchStartRe, ""),
4253
// Without property chars
43-
isUndef: chunk.replace(matchPropRe, ''),
54+
isUndef: chunk.replace(matchPropRe, ""),
4455
// Only remaining chars (can be zero chars)
45-
bothUndef: chunk
46-
.replace(matchStartRe, '')
47-
.replace(matchPropRe, ''),
48-
};
49-
});
56+
bothUndef: chunk.replace(_matchStartRe, "").replace(matchPropRe, ""),
57+
});
58+
}
59+
60+
return result;
61+
};
62+
63+
export { _makeChunks };

src/_makeQueue.mjs

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
1-
import { _find } from './_utils';
1+
import { __find, _find } from "./_utils.mjs";
2+
3+
const _sjsRegex = /__sjs/;
4+
5+
function _prepareQueue(originalSchema, queue, obj, acc = []) {
6+
if (_sjsRegex.test(obj)) {
7+
const usedAcc = [...acc];
8+
const find = __find(usedAcc);
9+
const { serializer } = find(originalSchema);
10+
11+
queue.push({
12+
serializer,
13+
find,
14+
name: acc[acc.length - 1],
15+
});
16+
return;
17+
}
18+
19+
// Recursively going deeper.
20+
// NOTE: While going deeper, the current prop is pushed into the accumulator
21+
// to keep track of the position inside of the object.
22+
const keys = Object.keys(obj);
23+
for (let i = 0; i < keys.length; ++i) {
24+
const key = keys[i];
25+
_prepareQueue(originalSchema, queue, obj[key], [...acc, key]);
26+
}
27+
}
228

329
/**
430
* @param {object} preparedSchema - schema already validated
531
* with modified prop values to avoid clashes.
632
* @param {object} originalSchema - User provided schema
733
* => contains array stringification serializers that are lost during preparation.
834
*/
9-
export default (preparedSchema, originalSchema) => {
35+
const _makeQueue = (preparedSchema, originalSchema) => {
1036
const queue = [];
11-
12-
// Defining a function inside an other function is slow.
13-
// However it's OK for this use case as the queue creation is not time critical.
14-
(function scoped(obj, acc = []) {
15-
if (/__sjs/.test(obj)) {
16-
const usedAcc = Array.from(acc);
17-
const find = _find(usedAcc);
18-
const { serializer } = find(originalSchema);
19-
20-
queue.push({
21-
serializer,
22-
find,
23-
name: acc[acc.length - 1],
24-
});
25-
return;
26-
}
27-
28-
// Recursively going deeper.
29-
// NOTE: While going deeper, the current prop is pushed into the accumulator
30-
// to keep track of the position inside of the object.
31-
return Object
32-
.keys(obj)
33-
.map(prop => scoped(obj[prop], [...acc, prop]));
34-
})(preparedSchema);
35-
37+
_prepareQueue(originalSchema, queue, preparedSchema);
3638
return queue;
3739
};
40+
41+
export { _makeQueue };
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
const _stringifyCallback = (_, value) => {
2+
if (!value.isSJS) return value;
3+
return `${value.type}__sjs`;
4+
};
15

26
/**
37
* `_prepare` - aims to normalize the schema provided by the user.
@@ -6,17 +10,13 @@
610
* @param {object} schema - user provided schema
711
*/
812
const _prepare = (schema) => {
9-
const preparedString = JSON.stringify(schema, (_, value) => {
10-
if (!value.isSJS) return value;
11-
return `${value.type}__sjs`;
12-
});
13-
14-
const preparedSchema = JSON.parse(preparedString);
13+
const _preparedString = JSON.stringify(schema, _stringifyCallback);
14+
const _preparedSchema = JSON.parse(_preparedString);
1515

1616
return {
17-
preparedString,
18-
preparedSchema,
17+
_preparedString,
18+
_preparedSchema,
1919
};
2020
};
2121

22-
export default _prepare;
22+
export { _prepare };

src/_select.js renamed to src/_select.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
* @param {any} value - value to serialize.
99
* @param {number} index - position inside the queue.
1010
*/
11-
const _select = chunks => (value, index) => {
11+
const _select = (chunks) => (value, index) => {
1212
const chunk = chunks[index];
1313

14-
if (typeof value !== 'undefined') {
14+
if (value !== undefined) {
1515
if (chunk.flag) {
1616
return chunk.prevUndef + value;
1717
}
@@ -28,4 +28,4 @@ const _select = chunks => (value, index) => {
2828
return chunk.isUndef;
2929
};
3030

31-
export default _select;
31+
export { _select };

src/_utils.mjs

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
/**
32
* `_find` is a super fast deep property finder.
43
* It dynamically build the function needed to reach the desired
@@ -11,16 +10,23 @@
1110
* @param {array} path - path to reach object property.
1211
*/
1312
const _find = (path) => {
14-
const { length } = path;
13+
let str = 'obj';
14+
15+
for (let i = 0; i < path.length; ++i) {
16+
str = `(${str}||{}).${path[i]}`;
17+
}
1518

19+
return eval(`(obj=>${str})`);
20+
};
21+
22+
const __find = (path) => {
1623
let str = 'obj';
1724

18-
for (let i = 0; i < length; i++) {
19-
str = str.replace(/^/, '(');
20-
str += ` || {}).${path[i]}`;
25+
for (let i = 0; i < path.length; ++i) {
26+
str += `?.['${path[i]}']`;
2127
}
2228

23-
return eval(`((obj) => ${str})`);
29+
return eval(`(obj=>${str})`);
2430
};
2531

2632
/**
@@ -35,51 +41,54 @@ const _makeArraySerializer = (serializer) => {
3541
return (array) => {
3642
// Stringifying more complex array using the provided sjs schema
3743
let acc = '';
38-
const { length } = array;
39-
for (let i = 0; i < length - 1; i++) {
44+
const len = array.length - 1;
45+
for (let i = 0; i < len; ++i) {
4046
acc += `${serializer(array[i])},`;
4147
}
4248

4349
// Prevent slice for removing unnecessary comma.
44-
acc += serializer(array[length - 1]);
50+
acc += serializer(array[len]);
4551
return `[${acc}]`;
4652
};
4753
}
4854

4955
return array => JSON.stringify(array);
5056
};
5157

52-
const TYPES = [
53-
'number',
54-
'string',
55-
'boolean',
56-
'array',
57-
'null',
58-
];
58+
const TYPES = ['number', 'string', 'boolean', 'array', 'null'];
5959

60-
const attr = (type, serializer) => {
61-
if (!TYPES.includes(type)) {
62-
throw new Error(`Expected one of: "number", "string", "boolean", "null". received "${type}" instead`);
60+
/*#__PURE__*/
61+
function checkType(type) {
62+
if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production' && !TYPES.includes(type)) {
63+
throw new Error(
64+
`Expected one of: "number", "string", "boolean", "array", "null". received "${type}" instead`,
65+
);
6366
}
67+
}
68+
69+
/**
70+
* @param type number|string|boolean|array|null
71+
* @param serializer
72+
* @returns
73+
*/
74+
const attr = (type, serializer) => {
75+
/*#__PURE__*/checkType(type);
6476

6577
const usedSerializer = serializer || (value => value);
6678

6779
return {
6880
isSJS: true,
6981
type,
70-
serializer: type === 'array'
71-
? _makeArraySerializer(serializer)
72-
: usedSerializer,
82+
serializer:
83+
type === 'array' ? _makeArraySerializer(serializer) : usedSerializer,
7384
};
7485
};
7586

7687
// Little utility for escaping convenience.
7788
// => if no regex is provided, a default one will be used.
78-
const defaultRegex = new RegExp('\\n|\\r|\\t|\\"|\\\\', 'gm');
79-
const escape = (regex = defaultRegex) => str => str.replace(regex, char => '\\' + char);
89+
const _defaultRegex = new RegExp('\\n|\\r|\\t|\\"|\\\\', 'gm');
90+
const _escapeCallback = char => '\\' + char;
91+
const escape = (regex = _defaultRegex) => str => str.replace(regex, _escapeCallback);
92+
93+
export { __find, _find, escape, attr };
8094

81-
export {
82-
_find,
83-
escape,
84-
attr,
85-
};

src/sjs.mjs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
import _prepare from './_prepare';
2-
import _makeQueue from './_makeQueue';
3-
import _makeChunks from './_makeChunks';
4-
import _select from './_select';
5-
import { attr, escape } from './_utils';
1+
import { _prepare } from "./_prepare.mjs";
2+
import { _makeQueue } from "./_makeQueue.mjs";
3+
import { _makeChunks } from "./_makeChunks.mjs";
4+
import { _select } from "./_select.mjs";
5+
import { attr, escape } from "./_utils.mjs";
66

77
// Doing a lot of preparation work before returning the final function responsible for
88
// the stringification.
99
const sjs = (schema) => {
10-
const { preparedString, preparedSchema } = _prepare(schema);
10+
const { _preparedString, _preparedSchema } = _prepare(schema);
1111

1212
// Providing preparedSchema for univocal correspondence between created queue and chunks.
1313
// Provided original schema to keep track of the original properties that gets destroied
1414
// during schema preparation => e.g. array stringification method.
15-
const queue = _makeQueue(preparedSchema, schema);
16-
const chunks = _makeChunks(preparedString, queue);
15+
const queue = _makeQueue(_preparedSchema, schema);
16+
const chunks = _makeChunks(_preparedString, queue);
1717
const selectChunk = _select(chunks);
1818

19-
const { length } = queue;
19+
const length = queue.length;
2020

2121
// Exposed function
2222
return (obj) => {
23-
let temp = '';
23+
let temp = "";
2424

2525
// Ditching old implementation for a **MUCH** faster while
2626
let i = 0;
@@ -31,7 +31,7 @@ const sjs = (schema) => {
3131

3232
temp += selectChunk(serializer(raw), i);
3333

34-
i += 1;
34+
++i;
3535
}
3636

3737
const { flag, pure, prevUndef } = chunks[chunks.length - 1];
@@ -40,8 +40,4 @@ const sjs = (schema) => {
4040
};
4141
};
4242

43-
export {
44-
sjs,
45-
attr,
46-
escape,
47-
};
43+
export { sjs, attr, escape };

0 commit comments

Comments
 (0)