跳转到内容

sharp — 让 Node.js 处理图像快到不像 JS

是什么

sharp 是 Node.js 处理图像的最快工具:把图片缩小、换格式、加水印这些事,用三行链式 JS 就能搞定,而且速度比传统 ImageMagick 快 4-5 倍、内存只占 1/4。

日常类比:像一个给图片做菜的流水线工厂。原料(一张大 JPEG)从一头进来,经过”切片→调味→换包装”三个工位,从另一头出来。关键是这条流水线不会把整批原料搬上工作台,而是一小块一小块(瓦片)流过来——所以处理 4K 大图也不爆内存。

写出来长这样:

const sharp = require('sharp')
await sharp('input.jpg').resize(800).webp().toFile('out.webp')

一行:读 JPEG、缩到宽 800、转 WebP 格式、写到磁盘。背后是 C 语言写的 libvips 库在干重活,sharp 只是把它包成 JS 友好的链式 API。

为什么重要

不理解 sharp,下面这些事都没法解释:

  • 为什么 Next.js / Astro / Vercel / Strapi / Gatsby 几乎所有 Node 框架的图像优化默认都装它(npm 周下载约 5000 万)
  • 为什么同一张 4K JPEG,sharp 处理只用 30MB 内存,ImageMagick 要 200MB+
  • 为什么 npm install sharp 不需要本地有 C 编译器也能装上(背后 9 个平台预编译二进制)
  • 为什么它已经 13 年还停在 0.x,但下游全都敢拿来生产

核心要点

sharp 的高速来自三个层叠的设计:

  1. 底层用 libvips 而不是 ImageMagick:libvips 是 1996 年起的 C 库,专门为 streaming 设计——把图片切成瓦片(tile)按需流过来,不全部加载。类比:ImageMagick 是把整箱菜搬上桌切,libvips 是流水线上一片片切。

  2. 链式调用 + 延迟执行 = 计算图融合:你写 .resize(800).rotate(90).blur(2)什么都没算,只是在搭一张”待办清单”。直到 .toFile() 才真正跑——而且相邻操作会被自动合并,比如”先裁剪再缩放”会被融合成”只采样需要的像素”。类比:跟服务员点 5 个菜,厨房统一规划火候,不是点一个炒一个。

  3. N-API + prebuild 二进制:JS 和 C 之间的桥用 N-API(一种 ABI 稳定接口),所以一份编译好的 .node 文件能跨 Node 14/16/18/20/22 跑;再配合 prebuild-install 自动下载 9 个平台 × arch 的预编译包,用户感受不到 native 模块的”难装”。

实践案例

案例 1:三行做一个 thumbnail

const sharp = require('sharp')
await sharp('photo.jpg')
.resize(400, 300, { fit: 'cover' })
.toFile('thumb.jpg')

逐部分解释:

  • sharp('photo.jpg') 不立即读图,只是创建一个”输入节点”
  • .resize(400, 300, { fit: 'cover' }) 加一个”缩放并填充”操作;fit: 'cover' 意思是”裁掉多余部分填满 400×300”
  • .toFile('thumb.jpg') 才触发真正的执行,返回 Promise

整个链没有 await 在中间——延迟执行让链式调用读起来像配方而不是步骤。

案例 2:用 Stream 处理大图不爆内存

const fs = require('fs')
const sharp = require('sharp')
fs.createReadStream('huge-4k.jpg')
.pipe(sharp().resize(1920).webp({ quality: 80 }))
.pipe(fs.createWriteStream('out.webp'))

逐部分解释:

  • fs.createReadStream 一次只读几 KB,不把整张 4K 图塞进内存
  • sharp() 不带参数时变成一个 Transform 流,可以接到管道里
  • 三段 pipe 串起来后,内存峰值大约固定在 30-50MB,无论原图多大

这就是为什么 Vercel / Cloudflare 的边缘图像优化敢用 sharp——并发上万张时,固定内存比”快”更可贵

案例 3:next/image 怎么调它

Next.js 的 <Image> 组件背后大致这样调 sharp:

async function optimizeImage(buffer, { width, format, quality }) {
let pipeline = sharp(buffer).rotate()
if (width) pipeline = pipeline.resize(width)
if (format === 'webp') pipeline = pipeline.webp({ quality })
return pipeline.toBuffer()
}

逐部分解释:

  • .rotate() 不传角度时,意思是”按 EXIF 自动旋正”——手机竖拍的照片会被纠正
  • pipeline = pipeline.xxx() 不可变(immutable),每次返回新链——所以可以条件分支构建
  • .toBuffer() 把结果当二进制返回,框架再缓存到 CDN

踩过的坑

  1. libvips 错误信息会”穿透”上来:报 VipsJpeg: out of order read at line 1024 这种 C 库内部状态机消息,对 JS 工程师极不友好——生产环境最好包一层错误规整化把它翻译成”图片损坏 / 格式不支持 / 内存不足”三类。

  2. bus factor = 1 的脆弱:Lovell Fuller 一个人维护 9 个平台 × 多 Node 版本 × 多 codec 的预编译矩阵,依赖大公司 sponsor。底层 libjpeg / libwebp 出 CVE 时升级压力极大。

  3. Edge Runtime 跑不了:Cloudflare Workers / Deno Deploy 等 Edge 环境不支持 N-API native binding,必须切到 wasm 路线(@squoosh),慢 2-3 倍且不能 streaming。

  4. 0.x 的”假稳定”:sharp 已 13 年仍在 0.33.x,下游必须 pin minor 版本不能 caret range。底层 libvips ABI 一升级就会在 binary 层 break,Docker 镜像构建失败常因为基础镜像没装 libvips-dev。

适用 vs 不适用场景

适用

  • Node.js SSR 服务端图像处理(Next.js / Astro / Strapi / Gatsby)
  • 需要高并发低内存(Vercel Lambda / Express 中间件)
  • 常见格式互转(JPEG / PNG / WebP / AVIF / TIFF)
  • 链式批处理(resize + crop + composite + format)

不适用

  • Edge Runtime(Cloudflare Workers / Bun-on-Edge)→ 用 wasm 路线(squoosh)
  • 浏览器内处理 → 用 Canvas API 或 wasm
  • 矢量图深加工(SVG 节点编辑)→ 用 svgo / sharp 只能 raster 化
  • PSD / 复杂动画 GIF 帧编辑 → 用 ag-psd / 专业工具
  • OCR / 视觉理解 → 用 Tesseract / 视觉大模型

历史小故事(可跳过)

  • 2013 年:Lovell Fuller 创建 sharp,最初只是 ImageMagick 的更快替代,几百行 JS。
  • 2014 年:切到 libvips backend,速度跳 4-5 倍——这是它真正与众不同的起点。
  • 2017 年:0.18 引入 N-API,从此一份编译产物跨所有 Node 版本可用。
  • 2019 年:稳定 prebuild 二进制覆盖 9 个平台,用户 npm install 不再卡在 node-gyp。
  • 2022 年:0.30 加入 AVIF 编解码,对齐 web 现代格式。
  • 至今:仍未发布 1.0——维护者宁愿不承诺 SemVer 也要保留底层 libvips 变动余地,这是工程师诚实。

学到什么

  1. C 库 + N-API 是 Node 性能解锁的正解——不要害怕 native 绑定,prebuild 解决了”难装”的负担。
  2. 链式 + 延迟执行 = 计算图融合的口子——这跟 Pandas / Spark / React 是同一种思路,是高性能 API 的通用配方。
  3. streaming 比 buffer 重要——并发场景下”固定内存”比”绝对快”更值钱。
  4. 0.x 长期稳定也行——SemVer 是社会承诺不是技术承诺,把它当工程师诚实就好。

延伸阅读

关联

  • jimp —— 纯 JS 图像库,sharp 跑不了时的 fallback 选项
  • fastify —— 同样靠”少包装、贴底层”做出 Node 高性能的代表
  • playwright —— 跨平台 native binary 分发的另一个范本
  • starlight —— Astro 文档站点主题,图像优化默认用 sharp
  • tanstack-router —— 同样链式 API + 延迟执行的设计哲学
  • biome —— Rust 写的工具链,跟 sharp 都属于”换语言换性能”路线

反向链接

  • biome —— Biome — JS/TS 工具链一体化(Rust 写的 linter+formatter)
  • fastify —— Fastify — 让 schema 替你写校验和序列化的 Node.js 框架
  • jimp —— jimp — 哪都能跑的纯 JS 图像处理库
  • opencv —— OpenCV — 开源计算机视觉库与跨平台图像视频处理
  • playwright —— Playwright — 跨浏览器自动化测试
  • starlight —— Starlight — Astro 文档站点主题
  • tanstack-router —— TanStack Router — 把 URL 当类型,编译器替你守路由