preface

Those of you who have developed with Node.js will be familiar with KOA because of its simple and elegant writing style, the rich community ecology, and the fact that many existing Node.js frameworks are repackaged based on KOA. However, in terms of performance, it is important to mention a well-known framework, Fastify, which, by its name, is characteristically fast, and official Benchmarks suggest it is even faster than Node.js’s native HTTP.server, too.

Key to performance improvement

Let’s start by looking at how Fastify launches a service.

# installation fastifyNPM I - S [email protected]Copy the code
// Create a service instance
const fastify = require('fastify')()

app.get('/', {
  schema: {
    response: {
      // Key indicates the response status code
      '200': {
        type: 'object'.properties: {
          hello: { type: 'string'}}}}}},async() = > {return { hello: 'world'}})// Start the service; (async() = > {try {
    const port = 3001 // Listen on the port
    await app.listen(port)
    console.info(`server listening on ${port}`)}catch (err) {
    console.error(err)
    process.exit(1)}}) ()Copy the code

As you can see from the above code, Fastify defines a schema for the response body of a request. In addition to defining the schema for the response body, Fastify also supports defining the following schemas for data:

  1. body: Validates the request body when the POST or PUT method is used.
  2. query: verifies url query parameters;
  3. params: verify URL parameters;
  4. response: filters and generates for the response bodyschema.
app.post('/user/:id', {
  schema: {
    params: {
      type: 'object'.properties: {
      	id: { type: 'number'}}},response: {
      // 2xx indicates that this schema applies to states from 200 to 299
      '2xx': {
        type: 'object'.properties: {
          id: { type: 'number' },
          name: { type: 'string'}}}}}},async (req) => {
  const id = req.params.id
  const userInfo = await User.findById(id)
  // Content-type Defaults to application/json
  return userInfo
})
Copy the code

The secret to fastify’s improved performance is that instead of using the native Json.stringify, it reimplements its own json serialization method. This schema is the key to doubling the performance of JSON serialization.

How do I serialize JSON

Before exploring how Fastify serializes JSON data, let’s take a look at how tedious json.stringify goes through, Here we refer to the Stringify method implemented in the open source JSON-JS by Douglas Crockford (creator of the JSON format).

JSON-js:github.com/douglascroc…

// show only the core json.stringify code, other code omitted
if (typeof JSON! = ="object") {
  JSON = {};
}
JSON.stringify = function (value) {
  return str("", {"": value})
}
function str(key, holder) {
  var value = holder[key];
  switch(typeof value) {
    case "string":
      return quote(value);
    case "number":
      return (isFinite(value)) ? String(value) : "null";
    case "boolean":
    case "null":
      return String(value);
    case "object":
      if(! value) {return "null";
      }
      partial = [];
      if (Object.prototype.toString.apply(value) === "[object Array]") {
        // Handle arrays
        length = value.length;
        for (i = 0; i < length; i += 1) {
          // Each element needs to be handled separately
          partial[i] = str(i, value) || "null";
        }
        // Convert partial to "[...] "
        v = partial.length === 0
          ? "[]"
          : "[" + partial.join(",") + "]";
        return v;
      } else {
        // Process objects
        for (k in value) {
          if (Object.prototype.hasOwnProperty.call(value, k)) {
            v = str(k, value);
            if (v) {
              partial.push(quote(k) + ":"+ v); }}}// Convert partial to "{... }"
        v = partial.length === 0
          ? "{}"
        	: "{" + partial.join(",") + "}";
        returnv; }}}Copy the code

As can be seen from the above code, when serializing JSON objects, it is necessary to traverse all arrays and objects, judge the type one by one, and add “” to all keys, and it does not include some encode operations of special characters. However, with schema, these situations become much easier. Fastify officially makes JSON serialization a separate repository: fast-jSON-stringify, and later introduces AJV for validation. Here, in order to make it easier to read the code, we choose to look at the earlier version: 0.1.0, which is relatively simple logic and easy to understand.

[email protected]: github.com/fastify/fas…

function $Null (i) {
  return 'null'
}

function $Number (i) {
  var num = Number(i)
  if (isNaN(num)) {
    return 'null'
  } else {
    return String(num)
  }
}

function $String (i) {
  return '"' + i + '"'
}

function buildObject (schema, code, name) {
  // Serialize objects...
}

function buildArray (schema, code, name) {
  // Serialize array...
}

function build (schema) {
  var code = `
    'use strict'

    The ${$String.toString()}
    The ${$Number.toString()}
    ${$Null.toString()}
  `
  var main

  code = buildObject(schema, code, '$main')

  code += `; return $main `

  return (new Function(code))()
}

module.exports = build
Copy the code

Fast-json-stringify exposes a build method that takes a schema and returns a function ($main) that serializes the objects corresponding to the schema as follows:

const build = require('fast-json-stringify')

const stringify = build({
  type: 'object'.properties: {
    id: { type: 'number' },
    name: { type: 'string'}}})console.log(stringify)

const objString = stringify({
  id: 1.name: 'shenfq'
})
console.log(objString) // {"id":1,"name":"shenfq"}
Copy the code

After the build construct, the serialization method returned is as follows:

function $String (i) {
  return '"' + i + '"'
}
function $Number (i) {
  var num = Number(i)
  if (isNaN(num)) {
    return 'null'
  } else {
    return String(num)
  }
}
function $Null (i) {
  return 'null'
}
// Serialization method
function $main (obj) {
  var json = '{'

  json += '"id":'

  json += $Number(obj.id)
  json += ', '
  json += '"name":'

  json += $String(obj.name)

  json += '} '
  return json
}
Copy the code

As you can see, with schema as the support, the serialization logic suddenly becomes extremely simple, and the resulting JSON string only retains the required attributes, which is concise and efficient. Let’s go back and look at how buildObject generates the code inside $main:

function buildObject (schema, code, name) {
  // Construct a function
  code += `
    function ${name} (obj) {
      var json = '{'
  `
  var laterCode = ' '
  // Iterate over the schema properties
  const { properties } = schema
  Object.keys(properties).forEach((key, i, a) = > {
    // The key needs double quotation marks
    code += `
      json += 'The ${$String(key)}:'
    `
    // Transform value by nested
    const value = properties[key]
    const result = nested(laterCode, name, `.${key}`, value)

    code += result.code
    laterCode = result.laterCode

    if (i < a.length - 1) {
      code += 'json += \',\''
    }
  })

  code += ` json += '}' return json } `

  code += laterCode

  return code
}

function nested (laterCode, name, key, schema) {
  var code = ' '
  var funcName
  // Determine the type of value
  const type = schema.type
  switch (type) {
    case 'null':
      code += ` json += $Null() `
      break
    case 'string':
      code += `
      json += $String(obj${key})
      `
      break
    case 'number':
    case 'integer':
      code += `
      json += $Number(obj${key})
      `
      break
    case 'object':
      If value is an object, a new method is required to construct it
      funcName = (name + key).replace(/[-.\[\]]/g.' ')
      laterCode = buildObject(schema, laterCode, funcName)
      code += `
        json += ${funcName}(obj${key})
      `
      break
    case 'array':
      funcName = (name + key).replace(/[-.\[\]]/g.' ')
      laterCode = buildArray(schema, laterCode, funcName)
      code += `
        json += ${funcName}(obj${key})
      `
      break
    default:
      throw new Error(`${type} unsupported`)}return {
    code,
    laterCode
  }
}
Copy the code

Properties of type “object” are traversed once, and then processed twice for different types of value. If a new object is encountered, a new function is constructed to handle it.

// If it contains child objects
const stringify = build({
  type: 'object'.properties: {
    id: { type: 'number' },
    info: {
      type: 'object'.properties: {
        age: { type: 'number' },
        name: { type: 'string'},}}}})console.log(stringify.toString())
Copy the code
function $main (obj) {
  var json = '{'

  json += '"id":'

  json += $Number(obj.id)
  json += ', '
  json += '"info":'

  json += $maininfo(obj.info)

  json += '} '
  return json
}

// Child objects are processed by another function
function $maininfo (obj) {
  var json = '{'

  json += '"age":'

  json += $Number(obj.age)
  json += ', '
  json += '"name":'

  json += $String(obj.name)

  json += '} '
  return json
}
Copy the code

conclusion

Of course, there are other internal optimizations that fastify claims to be fast, such as the use of Radix Tree for the implementation of the routing library and the reuse of context objects (using the MidDIE library). This article just introduces one of them reflects the most important obvious optimization ideas, I hope you can gain after reading.