美文网首页
Node.js性能优化

Node.js性能优化

作者: imakan | 来源:发表于2019-01-03 18:11 被阅读0次

本文内容源自知乎,地址: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倍

jsonStringify

一个简单的示例:

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原生实现的Promisebluebird这样的第三方实现的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

StreamNode.js最基本的概念,Node.js内部的大部分与IO相关的模块,比如HTTPnetfsrepl,都是建立在各种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-clinicNearForm 开源的一款 Node.js 性能诊断工具,可以非常快速地定位性能问题。

npm i -g clinic
npm i -g autocannon

使用的时候,先开启服务进程:

clinic doctor -- node server.js

·

相关文章

  • 六年打怪升级,一路披荆斩棘,只为没有难用的Node.js

    阿里云于近日推出了Node.js性能平台,旨在为Node.js应用提供集性能监控、性能优化和故障排查为一体的Saa...

  • Node.js性能优化

    本文内容源自知乎,地址:https://zhuanlan.zhihu.com/p/50055740 仅仅是简单的升...

  • Android性能优化 - 消除卡顿

    性能优化系列阅读 Android性能优化 性能优化 - 消除卡顿 性能优化 - 内存优化 性能分析工具 - Tra...

  • Android性能优化 - 内存优化

    性能优化系列阅读 Android性能优化 性能优化 - 消除卡顿 性能优化- 内存优化 性能分析工具 - Trac...

  • 前端性能优化(中)

    性能优化调研系列文章 《前端性能优化(上)》 《前端性能优化(中)》 《前端性能优化(下)》 《前端性能优化(上)...

  • 前端性能优化(下)

    性能优化调研系列文章 《前端性能优化(上)》 《前端性能优化(中)》 《前端性能优化(下)》 《前端性能优化(中)...

  • Awesome Extra

    性能优化 性能优化模式 常见性能优化策略的总结 Spark 性能优化指南——基础篇 Spark 性能优化指南——高...

  • 常用的后端性能优化六种方式:缓存化+服务化+异步化等

    性能优化专题 前端性能优化 数据库性能优化 jvm和多线程优化 架构层面优化 缓存性能优化 常用的后端性能优化六大...

  • webpack 性能优化

    webpack性能优化 开发环境性能优化 生产环境性能优化 开发环境性能优化 优化打包构建速度 优化调试功能 生产...

  • iOS性能优化 - 整理

    本文主要包含: 性能优化 - 卡顿性能优化 - 耗电优化性能优化 - APP启动优化安装包瘦身 一  性能优化 -...

网友评论

      本文标题:Node.js性能优化

      本文链接:https://www.haomeiwen.com/subject/sijqrqtx.html