本文内容源自知乎,地址:https://zhuanlan.zhihu.com/p/50055740
仅仅是简单的升级Node.js版本就可以轻松的获得性能提升,因为几乎任何新版本的Nodejs都会比老版本性能更好、
nodejs每个版本的性能提升主要来自于两个方面:
- v8的版本更新
- nodejs内部代码的更新优化
如何选择Nodejs的版本
nodejs的版本策略:
- nodejs的笨笨主要分为Current和LTS;
- Current就是当前最新的、依然处于开发中的nodejs版本
- LTS就是稳定、会长期维护的版本
- nodejs每六个月(每年的四月和十月)会发布一次大版本升级,大版本会带来一些不兼容的升级
- 每年4月发布的版本通常是偶数,是LTS版本,即长期支持的版本,社区会从发布当年的十月开始,继续维护18+12个月(Active LTS + Maintenance LTS)
- 每年十月分布的版本,版本号为奇数,只有8个月的维护期
举个例子,现在nodejs的版本是v11,LTS版本是V10和v8,更老的V6处于Maintenace LTS,从明年4月份起(18+12个月了)就不再维护,去年发布的v9版本今年6月就不在维护(奇数版本8个月)
使用fast-json-stringify加速JSON序列化
在JavaScript
中,生成json
字符串是非常方便的
const json = JSON.stringify(obj)
JSON.stringify也存在性能优化的空间,那就是使用JSON Schema来加速序列化。
在JSON
序列化时,我们需要识别大量的字段类型,比如对于string
类型,我们就需要两边加上""
,对于数组类型,我们需要遍历数组,把每个对象序列化后,用,
隔开,然后在两边加上[
和 ]
,诸如此类。如果我们已经提前通过Schema
知道每个字段的类型,那么就不需要遍历,识别字段类型,而可以直接用序列化对应的字段,这就大大减少了计算开销,这就是fast-json-stringfy的原理,在项目的跑分中,在某些情况下甚至可以比JSON.stringify
快接近10倍
一个简单的示例:
const fastJson = require('fast-json-stringify')
const stringify = fastJson({
title:'Example Schema',
type:'object',
properties:{
name:{type:'string'},
age:{type:'integer'},
books:{
type:'array',
item:{
type:'array',
uniqueItems:true
}
}
}
})
console.log(stringify({
name: 'Starkwang',
age: 23,
books: ['C++ Primer', '響け!ユーフォニアム~']
}))
//=> {"name":"Starkwang","age":23,"books":["C++ Primer","響け!ユーフォニアム~"]}
在node.js
的中间件业务中,通常会有很多数据使用json
进行传输,并且这些json
的结构非常相似(如果你使用了TypeScript,更是这样),这中场景就非常适用JSON Schema
来优化。
提升Promise
的性能
性能损耗主要来自于Promise
对象自身的实现,V8原生实现的Promise
比bluebird
这样的第三方实现的Promise
库要慢很多。而async/await
语法并不会带来太多的性能损失。
所以对于大量异步逻辑,轻量计算的中间件项目而言,可以在代码中把全局的Promise
换为bluebird
的实现
global.Promise = require('bluebird')
正确地编写异步代码
使用async/await
之后,项目的一步代码会非常好看
const foo = await doSomethingAsync();
const bar = await doSomethingElseAsync();
但因此,有时我们也会忘记使用Promise
给我们带来的其他能力,比如Promise.all()
的并行能力
// bad
async function getUserInfo(id) {
const profile = await getUserProfile(id);
const repo = await getUserRepo(id)
return { profile, repo }
}
// good
async function getUserInfo(id) {
const [profile, repo] = await Promise.all([
getUserProfile(id),
getUserRepo(id)
])
return { profile, repo }
}
还有比如Promise.any()
(此方法不在ES6 Promise标准中,也可以使用标准的Promise.race()
代替),我们可以用他轻松实现更加可靠快速的调用
async function getServiceIP(name) {
// 从 DNS 和 ZooKeeper 获取服务 IP,哪个先成功返回用哪个
// 与 Promise.race 不同的是,这里只有当两个调用都 reject 时,才会抛出错误
return await Promise.any([
getIPFromDNS(name),
getIPFromZooKeeper(name)
])
}
优化V8 GC
我们在日常开发代码的时候,比较容易踩到下面的几个坑:
使用大对象作为缓存,导致老生代(Old space)的垃圾回收变慢
示例:
const cache = {}
async function getUserInfo(id) {
if(!cache[id]){
cache[id] = await getUserInfoFromDatabase(id)
}
return cache[id]
}
这里我们使用了一个变量cache
作为缓存,加速用户信息的查询,进行了很多次查询后,cache
对象会进入老生代,并且会变得无比庞大,而老生代是使用三色标记+DFS
的方式进行GC
的,一个大对象会直接导致GC花费的时间增长(而且也有内存泄露的风险)
解决方法就是:
- 使用
redis
这样的外部缓存,实际上像Redis
这样的内存型数据库非常适合这种场景 - 限制本地缓存对象的大小,比如使用FIFO,TTL之类的机制来清理对象中的缓存
新生代空间不足,导致频繁GC
这个坑比较隐蔽
node
默认给新生代分配的内存是64MB(64位的机器),但因为新生代GC
使用的是Scavenge
算法,所以实际使用的内存只有一半,即32MB。
当业务代码频繁地产生大量的小对象时,这个空间很容易被占满,从而触发GC
。虽然新生代的GC
比老生代要快得多,但是频繁的GC
依然很大地影响性能,极端的情况下, GC
甚至可以占用全部计算时间的30%左右。
解决方法就是,在启动node.js
时,修改新生代的内存上限,减少GC
的次数:
node --max-semi-space-size=128 app.js
也不是内存越大越好,因为随着内存的增加,GC的次数减少,但是每次GC所需的时间也会增加,所以并不是越大越好。但是一般根据经验而言,分配64MB或者128MB是比较合理的
正确的使用Stream
Stream
是Node.js
最基本的概念,Node.js
内部的大部分与IO
相关的模块,比如HTTP
、net
、fs
、repl
,都是建立在各种Stream
之上的。
下面这个经典的例子,对于大文件,我们不需要把他完全读入内存,而是使用Stream
流式地把他发送出去
const http = require('http');
const fs = require('fs');
// bad
http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, data) {
res.end(data);
});
});
// good
http.createServer(function (req, res) {
const stream = fs.createReadStream(__dirname + '/data.txt');
stream.pipe(res);
});
使用pipeline管理stream
示例:
const {pipeline} = require('stream');
const fs = require('fs');
const zlib = require('zlib');
pipeline(
fs.createReadStream('archive.tar'),
zlib.createGzip(),
fs.createWriteStream('archive.tar.gz'),
(err) => {
if (err) {
console.error('Pipeline failed', err);
} else {
console.log('Pipeline succeeded');
}
}
)
使用node-clinic
快速定位性能问题
node-clinic 是 NearForm 开源的一款 Node.js 性能诊断工具,可以非常快速地定位性能问题。
npm i -g clinic
npm i -g autocannon
使用的时候,先开启服务进程:
clinic doctor -- node server.js
·
网友评论