preface

Faster HTTP request response times mean better performance for Web frameworks. The HTTP protocol is a text protocol, and the transmission format is string, whereas in our code we often manipulate data in JSON format. Therefore, you need to serialize the JSON data to a string before returning the response data. JavaScript natively provides a json. stringify method to convert JSON to a string. Let’s start with this method.

Slow JSON. Stringify

Because JavaScript is a dynamic language, its type is determined at run time, so json.stringify does a lot of typing and handles different key values for different types. Since we can’t do static analysis, we can’t do further optimization. There is also the need for layer upon layer recursion, and the risk of bursting the stack with circular references. In Fastify, the famously high-performance Node.js framework, json serialization performance is doubled by using the fast-json-stringify library instead of json.stringify. So how does fast-Json-stringify do it?

Fast – json – the stringify revelation

Fast-json-stringify defines the data format of (JSON) objects based on JSON Schema Draft 7. For example, objects:

{
    foo: 1, 
    bar: "abc"
}
Copy the code

Its JSON Schema can look like this:

{
  type: "object",
  properties: {
    foo: {type: "integer"},
    bar: {type: "string"}
  },
  required: ["foo"]
}
Copy the code

In addition to this simple type definition, JSON Schema also supports conditional operations, such as field types that can be strings or numbers, using the oneOf keyword:

"oneOf": [
  {
    "type": "string"
  },
  {
    "type": "number"
  }
]
Copy the code

For a complete definition of JSON Schema, refer to the documentation for Ajv, a popular JSON Schema validation tool with excellent performance. Here’s a simple code that uses fast-json-stringify:

require('http').createServer(handle).listen(3000)
var flatstr = require('flatstr')

var stringify = require('fast-json-stringify') ({type: 'object'.properties: { hello: { type: 'string'}}})function handle (req, res) {
  res.setHeader('Content-Type'.'application/json')
  res.end(flatstr(stringify({ hello: 'world'}})))Copy the code

In this code, fast-json-stringify takes a JSON Schema object as a parameter and generates a Stringify function. Usually, the data structure of Response is fixed, so it can be defined as a Schema, which is equivalent to telling the Stringify function the data structure of the object to serialize in advance so that it doesn’t have to make type decisions at run time. Here’s how the Stringify function is generated.

Generates the stringify function

First, you need to validate the JSON Schema. The underlying verification logic is implemented based on Ajv and will not be described here. You then need to pre-inject some utility methods for converting common types into strings.

const asFunctions = ` function $asAny (i) { return JSON.stringify(i) } function $asNull () { return 'null' } function $asInteger (i) { if (isLong && isLong(i)) { return i.toString() } else if (typeof i === 'bigint') { return i.toString() } else if (Number.isInteger(i)) { return $asNumber(i) } else { return $asNumber(parseInteger(i)) } } function $asNumber (i) { const num = Number(i) if (isNaN(num)) { return 'null' } else { return '' + num } } function $asBoolean (bool) { Return a bool && 'true' | | 'false'} / / omitted some other types of... `Copy the code

As you can see, using any, it still uses json.stringify inside. We often advise TS developers to avoid using any for good reason, since using any can also affect the performance of JSON serialization if the Schema is generated based on TS interface.

Next, you iterate over the schema, calling the corresponding utility functions for the different types to generate the code.

let code = `
    'use strict'
    ${asFunctions}
`
let main
switch (schema.type) {
    // Omit objects and arrays
    case 'integer':
        main = '$asInteger'
        break
    case 'number':
        main = '$asNumber'
        break
    case 'boolean':
        main = '$asBoolean'
        break
    case 'null':
        main = '$asNull'
        break
    case undefined:
        main = '$asAny'
        break
    default:
        throw new Error(`${schema.type} unsupported`)
}

code += `; return${main}
`
Copy the code

Finally, the Function constructor is called on the generated code.

const dependencies = [new Ajv(options.ajv)]
const dependenciesName = ['ajv']
dependenciesName.push(code)
return (Function.apply(null, dependenciesName).apply(null, dependencies))
Copy the code

The ajV object is injected into the function as an argument to handle if, then, else, anyOf, and so on in JSON Schema.

In addition, since the new Function is ultimately called to execute the code dynamically, this is actually a security risk. Therefore, developers are advised not to use user-generated schemas to ensure that the generated schemas are secure and controllable.

Finally, the developer calls the stringify function to turn the JSON into a string. The process of executing Stringify is essentially string concatenation.

conclusion

Fastify uses fast-json-stringify instead of json.stringify for faster JSON serialization. It allows the framework to know the structure of the JSON data in advance by pre-defining the JSON Schema by the developer. It then generates a Stringify function based on the JSON Schema, and all stringify does internally is concatenate strings. Finally, the developer calls the stringify function to serialize JSON. Essentially moving type analysis from run time to compile time.


Welcome to add wechat XXR0314 chat about technology, life, my wechat public account: bear writing place