Skip to content

Commit f2a5cf9

Browse files
committed
Improve perf via isJSON option
This adds some micro-optimizations along with a new macro optimization: `{isJSON: true}` option. Passing a `replacer` function to `JSON.stringify()` slows it down dramatically. If possible, we should avoid this this path, and the new `isJSON` option is the signal that the object passed-in contains no functions or regexp values. Node v0.12.10 simpleObj: JSON.stringify( simpleObj ) x 1,303,349 ops/sec ±0.66% (99 runs sampled) JSON.stringify( simpleObj ) with replacer x 386,634 ops/sec ±0.72% (96 runs sampled) serialize( simpleObj ) x 284,535 ops/sec ±0.92% (98 runs sampled) **serialize( simpleObj, {isJSON: true} ) x 951,798 ops/sec ±0.92% (96 runs sampled)**
1 parent 0b85bf8 commit f2a5cf9

File tree

4 files changed

+99
-20
lines changed

4 files changed

+99
-20
lines changed

index.js

+44-17
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,69 @@ var isRegExp = require('util').isRegExp;
1010

1111
// Generate an internal UID to make the regexp pattern harder to guess.
1212
var UID = Math.floor(Math.random() * 0x10000000000).toString(16);
13-
var PLACE_HOLDER_REGEXP = new RegExp('"@__(FUNCTION|REGEXP)-' + UID + '-(\\d+)__@"', 'g');
13+
var PLACE_HOLDER_REGEXP = new RegExp('"@__(F|R)-' + UID + '-(\\d+)__@"', 'g');
1414

1515
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
1616
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
1717

1818
// Mapping of unsafe HTML and invalid JavaScript line terminator chars to their
1919
// Unicode char counterparts which are safe to use in JavaScript strings.
20-
var UNICODE_CHARS = {
20+
var ESCAPED_CHARS = {
2121
'<' : '\\u003C',
2222
'>' : '\\u003E',
2323
'/' : '\\u002F',
2424
'\u2028': '\\u2028',
2525
'\u2029': '\\u2029'
2626
};
2727

28-
module.exports = function serialize(obj, space) {
28+
function escapeUnsafeChars(unsafeChar) {
29+
return ESCAPED_CHARS[unsafeChar];
30+
}
31+
32+
module.exports = function serialize(obj, options) {
33+
options || (options = {});
34+
35+
// Backwards-compatability for `space` as the second argument.
36+
if (typeof options === 'number' || typeof options === 'string') {
37+
options = {space: options};
38+
}
39+
2940
var functions = [];
3041
var regexps = [];
31-
var str;
3242

33-
// Creates a JSON string representation of the object and uses placeholders
34-
// for functions and regexps (identified by index) which are later
35-
// replaced.
36-
str = JSON.stringify(obj, function (key, value) {
37-
if (typeof value === 'function') {
38-
return '@__FUNCTION-' + UID + '-' + (functions.push(value) - 1) + '__@';
43+
// Returns placeholders for functions and regexps (identified by index)
44+
// which are later replaced by their string representation.
45+
function replacer(key, value) {
46+
if (!value) {
47+
return value;
48+
}
49+
50+
var type = typeof value;
51+
52+
if (type === 'object') {
53+
if (isRegExp(value)) {
54+
return '@__R-' + UID + '-' + (regexps.push(value) - 1) + '__@';
55+
}
56+
57+
return value;
3958
}
4059

41-
if (typeof value === 'object' && isRegExp(value)) {
42-
return '@__REGEXP-' + UID + '-' + (regexps.push(value) - 1) + '__@';
60+
if (type === 'function') {
61+
return '@__F-' + UID + '-' + (functions.push(value) - 1) + '__@';
4362
}
4463

4564
return value;
46-
}, space);
65+
}
66+
67+
var str;
68+
69+
// Creates a JSON string representation of the value.
70+
// NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args.
71+
if (options.isJSON && !options.space) {
72+
str = JSON.stringify(obj);
73+
} else {
74+
str = JSON.stringify(obj, options.isJSON ? null : replacer, options.space);
75+
}
4776

4877
// Protects against `JSON.stringify()` returning `undefined`, by serializing
4978
// to the literal string: "undefined".
@@ -54,9 +83,7 @@ module.exports = function serialize(obj, space) {
5483
// Replace unsafe HTML and invalid JavaScript line terminator chars with
5584
// their safe Unicode char counterpart. This _must_ happen before the
5685
// regexps and functions are serialized and added back to the string.
57-
str = str.replace(UNSAFE_CHARS_REGEXP, function (unsafeChar) {
58-
return UNICODE_CHARS[unsafeChar];
59-
});
86+
str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars);
6087

6188
if (functions.length === 0 && regexps.length === 0) {
6289
return str;
@@ -66,7 +93,7 @@ module.exports = function serialize(obj, space) {
6693
// string with their string representations. If the original value can not
6794
// be found, then `undefined` is used.
6895
return str.replace(PLACE_HOLDER_REGEXP, function (match, type, valueIndex) {
69-
if (type === 'REGEXP') {
96+
if (type === 'R') {
7097
return regexps[valueIndex].toString();
7198
}
7299

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Serialize JavaScript to a superset of JSON that includes regular expressions and functions.",
55
"main": "index.js",
66
"scripts": {
7-
"benchmark": "node test/benchmark/serialize.js",
7+
"benchmark": "node -v && node test/benchmark/serialize.js",
88
"test": "istanbul cover -- ./node_modules/mocha/bin/_mocha test/unit/ --reporter spec"
99
},
1010
"repository": {

test/benchmark/serialize.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

3-
var Benchmark = require('benchmark'),
4-
serialize = require('../../');
3+
var Benchmark = require('benchmark');
4+
var serialize = require('../../');
55

66
var suiteConfig = {
77
onStart: function (e) {
@@ -31,6 +31,14 @@ new Benchmark.Suite('simpleObj', suiteConfig)
3131
.add('JSON.stringify( simpleObj )', function () {
3232
JSON.stringify(simpleObj);
3333
})
34+
.add('JSON.stringify( simpleObj ) with replacer', function () {
35+
JSON.stringify(simpleObj, function (key, value) {
36+
return value;
37+
});
38+
})
39+
.add('serialize( simpleObj, {isJSON: true} )', function () {
40+
serialize(simpleObj, {isJSON: true});
41+
})
3442
.add('serialize( simpleObj )', function () {
3543
serialize(simpleObj);
3644
})

test/unit/serialize.js

+44
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,48 @@ describe('serialize( obj )', function () {
169169
expect(eval(serialize('</script>'))).to.equal('</script>');
170170
});
171171
});
172+
173+
describe('options', function () {
174+
it('should accept options as the second argument', function () {
175+
expect(serialize('foo', {})).to.equal('"foo"');
176+
});
177+
178+
it('should accept a `space` option', function () {
179+
expect(serialize([1], {space: 0})).to.equal('[1]');
180+
expect(serialize([1], {space: ''})).to.equal('[1]');
181+
expect(serialize([1], {space: undefined})).to.equal('[1]');
182+
expect(serialize([1], {space: null})).to.equal('[1]');
183+
expect(serialize([1], {space: false})).to.equal('[1]');
184+
185+
expect(serialize([1], {space: 1})).to.equal('[\n 1\n]');
186+
expect(serialize([1], {space: ' '})).to.equal('[\n 1\n]');
187+
expect(serialize([1], {space: 2})).to.equal('[\n 1\n]');
188+
});
189+
190+
it('should accept a `isJSON` option', function () {
191+
expect(serialize('foo', {isJSON: true})).to.equal('"foo"');
192+
expect(serialize('foo', {isJSON: false})).to.equal('"foo"');
193+
194+
function fn() { return true; }
195+
196+
expect(serialize(fn)).to.equal('function fn() { return true; }');
197+
expect(serialize(fn, {isJSON: false})).to.equal('function fn() { return true; }');
198+
199+
expect(serialize(fn, {isJSON: true})).to.equal('undefined');
200+
});
201+
});
202+
203+
describe('backwards-compatability', function () {
204+
it('should accept `space` as the second argument', function () {
205+
expect(serialize([1], 0)).to.equal('[1]');
206+
expect(serialize([1], '')).to.equal('[1]');
207+
expect(serialize([1], undefined)).to.equal('[1]');
208+
expect(serialize([1], null)).to.equal('[1]');
209+
expect(serialize([1], false)).to.equal('[1]');
210+
211+
expect(serialize([1], 1)).to.equal('[\n 1\n]');
212+
expect(serialize([1], ' ')).to.equal('[\n 1\n]');
213+
expect(serialize([1], 2)).to.equal('[\n 1\n]');
214+
});
215+
});
172216
});

0 commit comments

Comments
 (0)