Node.js 服務(wù)性能翻倍的秘密之一
前言
用過 Node.js 開發(fā)過的同學(xué)肯定都上手過 koa,因?yàn)樗?jiǎn)單優(yōu)雅的寫法,再加上豐富的社區(qū)生態(tài),而且現(xiàn)存的許多 Node.js 框架都是基于 koa 進(jìn)行二次封裝的。但是說到性能,就不得不提到一個(gè)知名框架:fastify ,聽名字就知道它的特性就是快,官方給出的Benchmarks甚至比 Node.js 原生的 http.Server 還要快。
Benchmarks
性能提升的關(guān)鍵
我們先看看 fastify 是如何啟動(dòng)一個(gè)服務(wù)的。
- # 安裝 fastify
- npm i -S fastify@3.9.1
- // 創(chuàng)建服務(wù)實(shí)例
- const fastify = require('fastify')()
- app.get('/', {
- schema: {
- response: {
- // key 為響應(yīng)狀態(tài)碼
- '200': {
- type: 'object',
- properties: {
- hello: { type: 'string' }
- }
- }
- }
- }
- }, async () => {
- return { hello: 'world' }
- })
- // 啟動(dòng)服務(wù)
- ;(async () => {
- try {
- const port = 3001 // 監(jiān)聽端口
- await app.listen(port)
- console.info(`server listening on ${port}`)
- } catch (err) {
- console.error(err)
- process.exit(1)
- }
- })()
從上面代碼可以看出,fastify 對(duì)請(qǐng)求的響應(yīng)體定義了一個(gè) schema,fastify 除了可以定義響應(yīng)體的 schema,還支持對(duì)如下數(shù)據(jù)定義 schema:
- body:當(dāng)為 POST 或 PUT 方法時(shí),校驗(yàn)請(qǐng)求主體;
- query:校驗(yàn) url 的 查詢參數(shù);
- params:校驗(yàn) url 參數(shù);
- response:過濾并生成用于響應(yīng)體的 schema。
- app.post('/user/:id', {
- schema: {
- params: {
- type: 'object',
- properties: {
- id: { type: 'number' }
- }
- },
- response: {
- // 2xx 表示 200~299 的狀態(tài)都適用此 schema
- '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 默認(rèn)為 application/json
- return userInfo
- })
讓 fastify 性能提升的的秘訣在于,其返回 application/json 類型數(shù)據(jù)的時(shí)候,并沒有使用原生的 JSON.stringify,而是自己內(nèi)部重新實(shí)現(xiàn)了一套 JSON 序列化的方法,這個(gè) schema 就是 JSON 序列化性能翻倍的關(guān)鍵。
如何對(duì) JSON 序列化
在探索 fastify 如何對(duì) JSON 數(shù)據(jù)序列化之前,我們先看看 JSON.stringify 需要經(jīng)過多么繁瑣的步驟,這里我們參考 Douglas Crockford (JSON 格式的創(chuàng)建者)開源的 JSON-js 中實(shí)現(xiàn)的 stringify 方法。
“JSON-js:https://github.com/douglascrockford/JSON-js/blob/master/json2.js
- // 只展示 JSON.stringify 核心代碼,其他代碼有所省略
- 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]") {
- // 處理數(shù)組
- length = value.length;
- for (i = 0; i < length; i += 1) {
- // 每個(gè)元素都需要單獨(dú)處理
- partial[i] = str(i, value) || "null";
- }
- // 將 partial 轉(zhuǎn)成 ”[...]“
- v = partial.length === 0
- ? "[]"
- : "[" + partial.join(",") + "]";
- return v;
- } else {
- // 處理對(duì)象
- for (k in value) {
- if (Object.prototype.hasOwnProperty.call(value, k)) {
- v = str(k, value);
- if (v) {
- partial.push(quote(k) + ":" + v);
- }
- }
- }
- // 將 partial 轉(zhuǎn)成 "{...}"
- v = partial.length === 0
- ? "{}"
- : "{" + partial.join(",") + "}";
- return v;
- }
- }
- }
從上面的代碼可以看出,進(jìn)行 JSON 對(duì)象序列化時(shí),需要遍歷所有的數(shù)組與對(duì)象,逐一進(jìn)行類型的判斷,并對(duì)所有的 key 加上 "",而且這里還不包括一些特殊字符的 encode 操作。但是,如果有了 schema 之后,這些情況會(huì)變得簡(jiǎn)單很多。fastify 官方將 JSON 的序列化單獨(dú)成了一個(gè)倉庫:fast-json-stringify,后期還引入了 ajv 來進(jìn)行校驗(yàn),這里為了更容易看懂代碼,選擇看比較早期的版本:0.1.0,邏輯比較簡(jiǎn)單,便于理解。
“fast-json-stringify@0.1.0:https://github.com/fastify/fast-json-stringify/blob/v0.1.0/index.js
- 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) {
- // 序列化對(duì)象 ...
- }
- function buildArray (schema, code, name) {
- // 序列化數(shù)組 ...
- }
- function build (schema) {
- var code = `
- 'use strict'
- ${$String.toString()}
- ${$Number.toString()}
- ${$Null.toString()}
- `
- var main
- code = buildObject(schema, code, '$main')
- code += `
- ;
- return $main
- `
- return (new Function(code))()
- }
- module.exports = build
fast-json-stringify 對(duì)外暴露一個(gè) build 方法,該方法接受一個(gè) schema,返回一個(gè)函數(shù)($main),用于將 schema 對(duì)應(yīng)的對(duì)象進(jìn)行序列化,具體使用方式如下:
- 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"}
經(jīng)過 build 構(gòu)造后,返回的序列化方法如下:
- 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'
- }
- // 序列化方法
- function $main (obj) {
- var json = '{'
- json += '"id":'
- json += $Number(obj.id)
- json += ','
- json += '"name":'
- json += $String(obj.name)
- json += '}'
- return json
- }
可以看到,有 schema 做支撐,序列化的邏輯瞬間變得無比簡(jiǎn)單,最后得到的 JSON 字符串只保留需要的屬性,簡(jiǎn)潔高效。我們回過頭再看看 buildObject 是如何生成 $main 內(nèi)的代碼的:
- function buildObject (schema, code, name) {
- // 構(gòu)造一個(gè)函數(shù)
- code += `
- function ${name} (obj) {
- var json = '{'
- `
- var laterCode = ''
- // 遍歷 schema 的屬性
- const { properties } = schema
- Object.keys(properties).forEach((key, i, a) => {
- // key 需要加上雙引號(hào)
- code += `
- json += '${$String(key)}:'
- `
- // 通過 nested 轉(zhuǎn)化 value
- 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
- // 判斷 value 的類型,不同類型進(jìn)行不同的處理
- 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':
- // 如果 value 為一個(gè)對(duì)象,需要一個(gè)新的方法進(jìn)行構(gòu)造
- 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
- }
- }
其實(shí)就是對(duì) type 為 "object" 的 properties 進(jìn)行一次遍歷,然后針對(duì) value 不同的類型進(jìn)行二次處理,如果碰到新的對(duì)象,會(huì)構(gòu)造一個(gè)新的函數(shù)進(jìn)行處理。
- // 如果包含子對(duì)象
- const stringify = build({
- type: 'object',
- properties: {
- id: { type: 'number' },
- info: {
- type: 'object',
- properties: {
- age: { type: 'number' },
- name: { type: 'string' },
- }
- }
- }
- })
- console.log(stringify.toString())
- function $main (obj) {
- var json = '{'
- json += '"id":'
- json += $Number(obj.id)
- json += ','
- json += '"info":'
- json += $maininfo(obj.info)
- json += '}'
- return json
- }
- // 子對(duì)象會(huì)通過另一個(gè)函數(shù)處理
- function $maininfo (obj) {
- var json = '{'
- json += '"age":'
- json += $Number(obj.age)
- json += ','
- json += '"name":'
- json += $String(obj.name)
- json += '}'
- return json
- }
總結(jié)
當(dāng)然,fastify 之所以號(hào)稱自己快,內(nèi)部還有一些其他的優(yōu)化方法,例如,在路由庫的實(shí)現(xiàn)上使用了 Radix Tree 、對(duì)上下文對(duì)象可進(jìn)行復(fù)用(使用 middie 庫)。本文只是介紹了其中的一種體現(xiàn)最重要明顯優(yōu)化思路,希望大家閱讀之后能有所收獲。
本文轉(zhuǎn)載自微信公眾號(hào)「 更了不起的前端」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 更了不起的前端公眾號(hào)。