Node.js 服务性能翻倍的秘密之一

开发 前端
用过 Node.js 开发过的同学肯定都上手过 koa,因为他简单优雅的写法,再加上丰富的社区生态,而且现存的许多 Node.js 框架都是基于 koa 进行二次封装的。

[[357408]]

前言

用过 Node.js 开发过的同学肯定都上手过 koa,因为他简单优雅的写法,再加上丰富的社区生态,而且现存的许多 Node.js 框架都是基于 koa 进行二次封装的。但是说到性能,就不得不提到一个知名框架:fastify ,听名字就知道它的特性就是快,官方给出的Benchmarks甚至比 Node.js 原生的 http.Server 还要快。

 

Benchmarks

性能提升的关键

我们先看看 fastify 是如何启动一个服务的。

  1. # 安装 fastify 
  2. npm i -S fastify@3.9.1 
  1. // 创建服务实例 
  2. const fastify = require('fastify')() 
  3.  
  4. app.get('/', { 
  5.   schema: { 
  6.     response: { 
  7.       // key 为响应状态码 
  8.       '200': { 
  9.         type: 'object'
  10.         properties: { 
  11.           hello: { type: 'string' } 
  12.         } 
  13.       } 
  14.     } 
  15.   } 
  16. }, async () => { 
  17.   return { hello: 'world' } 
  18. }) 
  19.  
  20. // 启动服务 
  21. ;(async () => { 
  22.   try { 
  23.     const port = 3001 // 监听端口 
  24.     await app.listen(port) 
  25.     console.info(`server listening on ${port}`) 
  26.   } catch (err) { 
  27.     console.error(err) 
  28.     process.exit(1) 
  29.   } 
  30. })() 

从上面代码可以看出,fastify 对请求的响应体定义了一个 schema,fastify 除了可以定义响应体的 schema,还支持对如下数据定义 schema:

  1. body:当为 POST 或 PUT 方法时,校验请求主体;
  2. query:校验 url 的 查询参数;
  3. params:校验 url 参数;
  4. response:过滤并生成用于响应体的 schema。
  1. app.post('/user/:id', { 
  2.   schema: { 
  3.     params: { 
  4.       type: 'object'
  5.       properties: { 
  6.        id: { type: 'number' } 
  7.       } 
  8.     }, 
  9.     response: { 
  10.       // 2xx 表示 200~299 的状态都适用此 schema 
  11.       '2xx': { 
  12.         type: 'object'
  13.         properties: { 
  14.           id: { type: 'number' }, 
  15.           name: { type: 'string' } 
  16.         } 
  17.       } 
  18.     } 
  19.   } 
  20. }, async (req) => { 
  21.   const id = req.params.id 
  22.   const userInfo = await User.findById(id) 
  23.   // Content-Type 默认为 application/json 
  24.   return userInfo 
  25. }) 

让 fastify 性能提升的的秘诀在于,其返回 application/json 类型数据的时候,并没有使用原生的 JSON.stringify,而是自己内部重新实现了一套 JSON 序列化的方法,这个 schema 就是 JSON 序列化性能翻倍的关键。

如何对 JSON 序列化

在探索 fastify 如何对 JSON 数据序列化之前,我们先看看 JSON.stringify 需要经过多么繁琐的步骤,这里我们参考 Douglas Crockford (JSON 格式的创建者)开源的 JSON-js 中实现的 stringify 方法。

“JSON-js:https://github.com/douglascrockford/JSON-js/blob/master/json2.js

  1. // 只展示 JSON.stringify 核心代码,其他代码有所省略 
  2. if (typeof JSON !== "object") { 
  3.   JSON = {}; 
  4. JSON.stringify = function (value) { 
  5.   return str("", {"": value}) 
  6. function str(key, holder) { 
  7.   var value = holder[key]; 
  8.   switch(typeof value) { 
  9.     case "string"
  10.       return quote(value); 
  11.     case "number"
  12.       return (isFinite(value)) ? String(value) : "null"
  13.     case "boolean"
  14.     case "null"
  15.       return String(value); 
  16.     case "object"
  17.       if (!value) { 
  18.         return "null"
  19.       } 
  20.       partial = []; 
  21.       if (Object.prototype.toString.apply(value) === "[object Array]") { 
  22.         // 处理数组 
  23.         length = value.length; 
  24.         for (i = 0; i < length; i += 1) { 
  25.           // 每个元素都需要单独处理 
  26.           partial[i] = str(i, value) || "null"
  27.         } 
  28.         // 将 partial 转成 ”[...]“ 
  29.         v = partial.length === 0 
  30.           ? "[]" 
  31.           : "[" + partial.join(",") + "]"
  32.         return v; 
  33.       } else { 
  34.         // 处理对象 
  35.         for (k in value) { 
  36.           if (Object.prototype.hasOwnProperty.call(value, k)) { 
  37.             v = str(k, value); 
  38.             if (v) { 
  39.               partial.push(quote(k) + ":" + v); 
  40.             } 
  41.           } 
  42.         } 
  43.         // 将 partial 转成 "{...}" 
  44.         v = partial.length === 0 
  45.           ? "{}" 
  46.          : "{" + partial.join(",") + "}"
  47.         return v; 
  48.       } 
  49.   } 

从上面的代码可以看出,进行 JSON 对象序列化时,需要遍历所有的数组与对象,逐一进行类型的判断,并对所有的 key 加上 "",而且这里还不包括一些特殊字符的 encode 操作。但是,如果有了 schema 之后,这些情况会变得简单很多。fastify 官方将 JSON 的序列化单独成了一个仓库:fast-json-stringify,后期还引入了 ajv 来进行校验,这里为了更容易看懂代码,选择看比较早期的版本:0.1.0,逻辑比较简单,便于理解。

“fast-json-stringify@0.1.0:https://github.com/fastify/fast-json-stringify/blob/v0.1.0/index.js

  1. function $Null (i) { 
  2.   return 'null' 
  3.  
  4. function $Number (i) { 
  5.   var num = Number(i) 
  6.   if (isNaN(num)) { 
  7.     return 'null' 
  8.   } else { 
  9.     return String(num) 
  10.   } 
  11.  
  12. function $String (i) { 
  13.   return '"' + i + '"' 
  14.  
  15. function buildObject (schema, code, name) { 
  16.   // 序列化对象 ... 
  17.  
  18. function buildArray (schema, code, name) { 
  19.   // 序列化数组 ... 
  20.  
  21. function build (schema) { 
  22.   var code = ` 
  23.     'use strict' 
  24.  
  25.     ${$String.toString()} 
  26.     ${$Number.toString()} 
  27.     ${$Null.toString()} 
  28.   ` 
  29.   var main 
  30.  
  31.   code = buildObject(schema, code, '$main'
  32.  
  33.   code += ` 
  34.     ; 
  35.     return $main 
  36.   ` 
  37.  
  38.   return (new Function(code))() 
  39.  
  40. module.exports = build 

fast-json-stringify 对外暴露一个 build 方法,该方法接受一个 schema,返回一个函数($main),用于将 schema 对应的对象进行序列化,具体使用方式如下:

  1. const build = require('fast-json-stringify'
  2.  
  3. const stringify = build({ 
  4.   type: 'object'
  5.   properties: { 
  6.     id: { type: 'number' }, 
  7.     name: { type: 'string' } 
  8.   } 
  9. }) 
  10. console.log(stringify) 
  11.  
  12. const objString = stringify({ 
  13.   id: 1, name'shenfq' 
  14. }) 
  15. console.log(objString) // {"id":1,"name":"shenfq"

经过 build 构造后,返回的序列化方法如下:

  1. function $String (i) { 
  2.   return '"' + i + '"' 
  3. function $Number (i) { 
  4.   var num = Number(i) 
  5.   if (isNaN(num)) { 
  6.     return 'null' 
  7.   } else { 
  8.     return String(num) 
  9.   } 
  10. function $Null (i) { 
  11.   return 'null' 
  12. // 序列化方法 
  13. function $main (obj) { 
  14.   var json = '{' 
  15.  
  16.   json += '"id":' 
  17.  
  18.   json += $Number(obj.id) 
  19.   json += ',' 
  20.   json += '"name":' 
  21.  
  22.   json += $String(obj.name
  23.  
  24.   json += '}' 
  25.   return json 

可以看到,有 schema 做支撑,序列化的逻辑瞬间变得无比简单,最后得到的 JSON 字符串只保留需要的属性,简洁高效。我们回过头再看看 buildObject 是如何生成 $main 内的代码的:

  1. function buildObject (schema, code, name) { 
  2.   // 构造一个函数 
  3.   code += ` 
  4.     function ${name} (obj) { 
  5.       var json = '{' 
  6.   ` 
  7.   var laterCode = '' 
  8.   // 遍历 schema 的属性 
  9.   const { properties } = schema 
  10.   Object.keys(properties).forEach((key, i, a) => { 
  11.     // key 需要加上双引号 
  12.     code += ` 
  13.       json += '${$String(key)}:' 
  14.     ` 
  15.     // 通过 nested 转化 value 
  16.     const value = properties[key
  17.     const result = nested(laterCode, name, `.${key}`, value) 
  18.  
  19.     code += result.code 
  20.     laterCode = result.laterCode 
  21.  
  22.     if (i < a.length - 1) { 
  23.       code += 'json += \',\'' 
  24.     } 
  25.   }) 
  26.  
  27.   code += ` 
  28.       json += '}' 
  29.       return json 
  30.     } 
  31.   ` 
  32.  
  33.   code += laterCode 
  34.  
  35.   return code 
  36.  
  37. function nested (laterCode, namekeyschema) { 
  38.   var code = '' 
  39.   var funcName 
  40.   // 判断 value 的类型,不同类型进行不同的处理 
  41.   const type = schema.type 
  42.   switch (type) { 
  43.     case 'null'
  44.       code += ` 
  45.       json += $Null() 
  46.       ` 
  47.       break 
  48.     case 'string'
  49.       code += ` 
  50.       json += $String(obj${key}) 
  51.       ` 
  52.       break 
  53.     case 'number'
  54.     case 'integer'
  55.       code += ` 
  56.       json += $Number(obj${key}) 
  57.       ` 
  58.       break 
  59.     case 'object'
  60.       // 如果 value 为一个对象,需要一个新的方法进行构造 
  61.       funcName = (name + key).replace(/[-.\[\]]/g, ''
  62.       laterCode = buildObject(schema, laterCode, funcName) 
  63.       code += ` 
  64.         json += ${funcName}(obj${key}) 
  65.       ` 
  66.       break 
  67.     case 'array'
  68.       funcName = (name + key).replace(/[-.\[\]]/g, ''
  69.       laterCode = buildArray(schema, laterCode, funcName) 
  70.       code += ` 
  71.         json += ${funcName}(obj${key}) 
  72.       ` 
  73.       break 
  74.     default
  75.       throw new Error(`${type} unsupported`) 
  76.   } 
  77.  
  78.   return { 
  79.     code, 
  80.     laterCode 
  81.   } 

其实就是对 type 为 "object" 的 properties 进行一次遍历,然后针对 value 不同的类型进行二次处理,如果碰到新的对象,会构造一个新的函数进行处理。

  1. // 如果包含子对象 
  2. const stringify = build({ 
  3.   type: 'object'
  4.   properties: { 
  5.     id: { type: 'number' }, 
  6.     info: { 
  7.       type: 'object'
  8.       properties: { 
  9.         age: { type: 'number' }, 
  10.         name: { type: 'string' }, 
  11.       } 
  12.     } 
  13.   } 
  14. }) 
  15.  
  16. console.log(stringify.toString()) 
  1. function $main (obj) { 
  2.   var json = '{' 
  3.  
  4.   json += '"id":' 
  5.  
  6.   json += $Number(obj.id) 
  7.   json += ',' 
  8.   json += '"info":' 
  9.  
  10.   json += $maininfo(obj.info) 
  11.  
  12.   json += '}' 
  13.   return json 
  14.  
  15. // 子对象会通过另一个函数处理 
  16. function $maininfo (obj) { 
  17.   var json = '{' 
  18.  
  19.   json += '"age":' 
  20.  
  21.   json += $Number(obj.age) 
  22.   json += ',' 
  23.   json += '"name":' 
  24.  
  25.   json += $String(obj.name
  26.  
  27.   json += '}' 
  28.   return json 

总结

当然,fastify 之所以号称自己快,内部还有一些其他的优化方法,例如,在路由库的实现上使用了 Radix Tree 、对上下文对象可进行复用(使用 middie 库)。本文只是介绍了其中的一种体现最重要明显优化思路,希望大家阅读之后能有所收获。

本文转载自微信公众号「 更了不起的前端」,可以通过以下二维码关注。转载本文请联系 更了不起的前端公众号。

 

责任编辑:武晓燕 来源: 更了不起的前端
相关推荐

2020-12-14 15:40:59

Nodefastifyjs

2020-12-28 08:48:44

JS工具fastify

2019-07-09 14:50:15

Node.js前端工具

2013-11-01 09:34:56

Node.js技术

2015-03-10 10:59:18

Node.js开发指南基础介绍

2022-08-28 16:30:34

Node.jsDocker指令

2020-05-29 15:33:28

Node.js框架JavaScript

2022-08-22 07:26:32

Node.js微服务架构

2020-10-12 08:06:28

HTTP 服务器证书

2012-02-03 09:25:39

Node.js

2015-12-14 10:39:14

2015-11-04 09:18:41

Node.js应用性能

2021-12-25 22:29:57

Node.js 微任务处理事件循环

2011-09-08 13:46:14

node.js

2011-09-02 14:47:48

Node

2011-09-09 14:23:13

Node.js

2011-11-01 10:30:36

Node.js

2009-11-05 10:45:58

WCF服务

2022-09-12 15:58:50

node.js微服务Web

2011-06-17 10:29:04

Nodejavascript
点赞
收藏

51CTO技术栈公众号