← querystring 模块 assert 断言测试 →

Node.js zlib模块指南:从Gzip、Deflate到Brotli的数据压缩

著
原创 2026-10-13 Node.js 已有人查阅

一、为什么Node.js开发者需要认真对待zlib

做后端开发时间久了,会发现一个规律:真正影响接口响应速度的,往往不是数据库查询本身,而是网络传输的数据体积。一个JSON接口返回200KB未压缩内容,在弱网环境下可能需要1.5秒以上才能传完;如果服务端开启Gzip压缩,同样的数据可能只剩30KB左右,传输时间直接降到200毫秒级别。

Node.js把这个能力内置到了zlib模块里,不需要额外安装任何第三方包。它底层调用的是C语言编写的zlib通用压缩库,这个库在操作系统和各类软件中已经存在了很多年,稳定性和性能都经过了充分验证。Node.js在此基础上做了封装,暴异步回调和流式两种使用方式。

需要明确一点:zlib模块不是简单的"压缩工具",它同时承担着解压缩、格式自动识别、流式管道处理等职责。理解它的方法分类和适用边界,比记住几个API名字更重要。

二、模块导入与基础语法

Node.js从16版本开始推荐使用node:前缀导入内置模块,这样能明确区分内置模块和第三方模块,避免命名冲突。

const zlib = require('node:zlib');

require('node:zlib')返回的是zlib模块对象,所有压缩解压方法都挂在这个对象上。const声明保证模块引用不会被重新赋值。如果项目使用ESModule写法,对应的是:

import zlib from 'node:zlib';

两种写能一致,选择哪种取决于项目的模块规范。在项目中,我倾向于统一使用node:前缀,这样在代码审查时能一眼看出哪些是内置能力、哪些是外部依赖。

三、核心方法分类与选型逻辑

zlib模块的方法可以按两个维度划分:算法维度(Gzip、Deflate、Brotli)和使用方式维度(一次性回调、流式处理)。

一次性回调方法适合处理小体积数据,比如几百KB以内的字符串或Buffer。流式方法适合处理大文件,因为流式处理不会把整个文件读进内存,而是分块读取、分块压缩、分块写入。

方法 作用 适用场景
zlib.gzip() Gzip压缩 HTTP响应压缩、日志归档
zlib.gunzip() Gzip解压 读取.gz文件
zlib.deflate() Deflate压缩 需要更小体积的场景
zlib.inflate() Deflate解压 解压Deflate数据
zlib.brotliCompress() Brotli压缩 文本类静态资源
zlib.brotliDecompress() Brotli解压 解压Brotli数据
zlib.createGzip() Gzip压缩流 大文件压缩
zlib.createGunzip() Gzip解压流 大文件解压
zlib.createUnzip() 自动识别解压流 不确定压缩格式时
zlib.createDeflate() Deflate压缩流 流式Deflate压缩
zlib.createInflate() Deflate解压流 流式Deflate解压
zlib.createBrotliCompress() Brotli压缩流 流式Brotli压缩
zlib.createBrotliDecompress() Brotli解压流 流式Brotli解压

选型时有一个经验判断:如果数据要经过HTTP传输且客户端是浏览器,Gzip的兼容性最稳妥;如果是纯文本且追求更小的体积,Brotli在同等压缩级别下通常比Gzip小15%到25%;Deflate现在单独使用的场景不多,更多是作为Gzip的底层算法存在。

四、Gzip压缩与解压

先看一个最基础的压缩示例。代码号学习编程的示例里,我们用一段短字符串来观察压缩后的Base64输出。

const zlib = require('node:zlib');

const input = '代码号学习编程';

zlib.gzip(input, (err, buffer) => {
    if (err) {
        console.error('压缩失败:', err.message);
        return;
    }
    console.log('压缩结果:', buffer.toString('base64'));
});

回调里的buffer是压缩后的Buffer对象,直接打印会看到一串十六进制字节,转成Base64更方便观察和传输。这里有个细节:zlib.gzip()的第一个参数可以是字符串、Buffer、TypedArray、DataView或ArrayBuffer,Node.js内部会做类型转换。

解压时把压缩后的Buffer传回去:

const zlib = require('node:zlib');

const input = '代码号学习编程';

zlib.gzip(input, (err, compressed) => {
    if (err) {
        console.error('压缩失败:', err.message);
        return;
    }

    zlib.gunzip(compressed, (err, result) => {
        if (err) {
            console.error('解压失败:', err.message);
            return;
        }
        console.log('解压结果:', result.toString('utf8'));
    });
});

这段代码的输出是解压结果:代码号学习编程。嵌套回调的写法在真实项目中容易形成回调地狱,后面会讲用Promise和async/await的改进方案。

五、Deflate压缩与解压

Deflate是Gzip的底层压缩算法,区别在于Gzip在Deflate数据外面包了一层头部和校验尾。单独使用Deflate的场景通常是内部协议通信,双方约定好格式,不需要Gzip的额外头部信息。

const zlib = require('node:zlib');
const input = '代码号学习编程';

zlib.deflate(input, (err, buffer) => {
    if (err) {
        console.error('Deflate 压缩失败:', err.message);
        return;
    }
    console.log('Deflate 压缩结果:', buffer.toString('base64'));

    zlib.inflate(buffer, (err, result) => {
        if (err) {
            console.error('Inflate 解压失败:', err.message);
            return;
        }
        console.log('还原结果:', result.toString('utf8'));
    });
});

注意console.log('Deflate压缩结果:...')这行在回调内部,而有些教程会把它写在回调外面,导致输出顺序看起来"压缩结果后打印"。这是异步执行的正常现象,不是bug。在项目里如果要保证顺序,要么放在回调里,要么用async/await改写。

六、Brotli压缩与解压

Brotli是Google在2015年推出的压缩算法,为Web文本资源设计。它的字典里预置了大量常见HTML、CSS、JavaScript关键词,所以对这类内容的压缩率明显优于Gzip。Node.js从10.16.0版本开始支持Brotli。

const zlib = require('node:zlib');
const input = '代码号学习编程';

zlib.brotliCompress(input, (err, buffer) => {
    if (err) {
        console.error('Brotli 压缩失败:', err.message);
        return;
    }
    console.log('Brotli 压缩结果:', buffer);

    zlib.brotliDecompress(buffer, (err, result) => {
        if (err) {
            console.error('Brotli 解压失败:', err.message);
            return;
        }
        console.log('Brotli 还原结果:', result.toString('utf8'));
    });
});

Brotli的默认参数在压缩率和速度之间做了平衡。如果追求更小体积,可以在options里调整params,比如把BROTLI_PARAM_QUALITY设为11,但压缩时间会显著增加。实际项目中,静态资源构建阶段可以用高质量参数,运行时动态压缩建议用默认值或更低质量,避免阻塞事件循环。

这里有一个容易踩的坑:Brotli压缩后的Buffer直接toString('base64')和toString('utf8')结果不同,传输时必须明确编码方式。和外部系统对接时,建议在协议文档里写清楚压缩算法和编码格式。

七、流式压缩:处理大文件的正确方式

一次性回调方法有个硬限制:数据必须全部读入内存。如果压缩一个2GB的日志文件,Node.js进程很可能直接OOM。流式处理的思路是分块读取、分块压缩、分块写入,内存占用只和块大小有关,和文件总体积无关。

pipeline()方法是Node.js官方推荐的流式管道连接方式,它会在任意环节出错时自动销毁所有流,避免资源泄漏。

const fs = require('node:fs');
const zlib = require('node:zlib');
const { pipeline } = require('node:stream');

fs.writeFileSync('codehub.log', '代码号学习编程 - 日志内容示例');

const readStream = fs.createReadStream('codehub.log');
const writeStream = fs.createWriteStream('codehub.log.gz');

pipeline(
    readStream,
    zlib.createGzip(),
    writeStream,
    (err) => {
        if (err) {
            console.error('压缩管道失败:', err.message);
            return;
        }
        console.log('日志文件压缩完成');
    }
);

这段代码把codehub.log压缩成codehub.log.gz。pipeline()的之后一个参数是回调,用来处理最终的错误或成功状态。和手动pipe()相比,pipeline()在错误处理上更省心,不需要自己监听每个流的error事件。

解压大文件同理,把zlib.createGzip()换成zlib.createGunzip()即可。如果压缩格式不确定,用zlib.createUnzip(),它会根据数据头自动判断是Gzip还是Deflate。

八、个人经验:压缩级别与性能的取舍

zlib的每个方法都接受一个options对象,其中level参数控制压缩级别,取值范围是0到9。0表示不压缩,1表示最快速度,9表示最高压缩率,默认值是6。

我在项目里做过一组测试:对一份10MB的JSON日志,level设为1时压缩耗时约80ms,压缩后体积1.8MB;level设为9时耗时约620ms,压缩后体积1.5MB。多花7倍时间,只换来17%的体积下降。对于实时性要求高的接口,这个交换并不划算。

所以我的建议是:运行时动态压缩用level4到6,构建阶段处理静态资源可以用9,日志归档这种对时间不敏感的场景也可以用9。Brotli的quality参数逻辑类似,默认值11在动态场景下偏慢,可以考虑降到5左右。

另一个经验是:不要对已经压缩过的数据再压缩。JPEG、PNG、MP4这些格式本身已经压缩过,再用Gzip处理,体积可能不降反升,还白白消耗CPU。判断依据是看文件MIME类型,image/jpeg、video/mp4、application/zip这类直接跳过压缩。

九、错误处理与类型安全

zlib的回调函数第一个参数是err,这个约定和Node.js其他异步API一致。常见错误包括:解压时数据格式不对、压缩级别超出范围、Buffer被截断等。

在TypeScript项目里,可以给回调参数加上类型标注:

import zlib from 'node:zlib';

const input: string = '代码号学习编程';

zlib.gzip(input, (err: Error | null, buffer: Buffer) => {
    if (err) {
        console.error('压缩异常:', err.message);
        return;
    }
    console.log('压缩后长度:', buffer.length);
});

如果项目里大量使用zlib,建议封装一层Promise版本,避免回调嵌套:

const zlib = require('node:zlib');
const { promisify } = require('node:util');

const gzipAsync = promisify(zlib.gzip);
const gunzipAsync = promisify(zlib.gunzip);

async function compressAndRestore(text) {
    const compressed = await gzipAsync(text);
    const restored = await gunzipAsync(compressed);
    return restored.toString('utf8');
}

compressAndRestore('代码号学习编程').then(console.log);

这种封装在Express或Fastify的中间件里很实用,可以用try/catch统一处理压缩异常。

十、本节课程知识要点

  • zlib是Node.js内置模块,用require('node:zlib')导入,无需安装第三方包

  • Gzip兼容性很好,Brotli对文本压缩率更高,Deflate多用于内部协议

  • 小数据用回调方法,大文件用pipeline()配合createGzip()等流式方法

  • 压缩级别6是默认值,动态场景建议4到6,静态资源可以用9

  • 已压缩格式(JPEG、PNG、MP4)不要再做Gzip压缩

  • 生产环境建议用promisify封装,配合async/await和try/catch做错误处理

十一、练习与思考

  1. 写一个函数,接收文件路径,自动判断是否已压缩,未压缩则用Gzip压缩并删除原文件,已压缩则跳过。

  2. 用zlib.createUnzip()改写第七节的解压示例,观察它能否正确识别Gzip和Deflate两种格式。

  3. 对比同一段JSON数据在Gziplevel6和Brotliquality5下的压缩体积与耗时,记录数据并分析差异。

  4. 思考:在HTTP服务中,如何根据请求头的Accept-Encoding字段动态选择压缩算法?尝试写出中间件伪代码。

zlib模块的API数量不多,但每个方法背后都涉及压缩算法、流式处理、异步编程几个知识点的交叉。刚开始接触时,先把Gzip的压缩和解压跑通,再理解流式处理的必要性,之后根据项目场景选择算法和参数。项目开发中遇到压缩相关的问题,优先检查三件事:数据是否已经压缩过、压缩级别是否合理、流式管道是否用了pipeline()。把这三点处理好,大部分压缩场景都不会出问题。

← querystring 模块 assert 断言测试 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号