PascalEugster/payloadcms-plugin-image-optimizer
GitHub: PascalEugster/payloadcms-plugin-image-optimizer
这是一个用于 Payload CMS 3.x 的自动图片优化插件,支持将上传的图片转换为 WebP/AVIF 格式并进行压缩与元数据剥离。
Stars: 12 | Forks: 0
# @inoo-ch/payload-image-optimizer
[](https://www.npmjs.com/package/@inoo-ch/payload-image-optimizer)
[](https://www.npmjs.com/package/@inoo-ch/payload-image-optimizer)
[](https://github.com/PascalEugster/payloadcms-plugin-image-optimizer)
一个用于自动优化图片的 [Payload CMS](https://payloadcms.com) 插件。将上传的图片转换为 WebP/AVIF,调整至可配置的尺寸限制,去除 EXIF 元数据,生成 [ThumbHash](https://evanw.github.io/thumbhash/) 模糊占位符,并支持从管理面板批量重新生成。
由 [inoo.ch](https://inoo.ch) 构建和维护 —— 一家致力于打造现代网络体验的瑞士数字代理商。
## 功能
- **格式转换** — 自动生成可配置质量的 WebP 和 AVIF 变体
- **智能调整大小** — 将图片限制在最大尺寸内,同时保持宽高比
- **EXIF 剥离** — 移除元数据以减小文件体积并提升隐私保护
- **ThumbHash 占位符** — 生成微小的模糊哈希,实现即时图片预览
- **批量重新生成** — 通过管理 UI 重新处理现有图片,并附带进度跟踪
- **按集合配置** — 可针对每个集合覆盖格式、质量和尺寸
- **管理 UI** — 侧边栏显示状态徽章、节省的文件大小以及模糊预览
- **ImageBox 组件** — 即插即用的 Next.js `` 封装,支持 ThumbHash 模糊、淡入效果、响应式变体加载和智能 `sizes` 默认值
- **响应式变体加载器** — 直接提供预生成的 Payload 尺寸变体,绕过 `/_next/image` 的重复优化
- **模板友好** — `getOptimizedImageProps()` 仅需 3 行代码即可集成到 Payload 网站模板中
- **FadeImage 组件** — 独立的淡入图片组件,适用于使用 `getImageOptimizerProps()` 的自定义设置
## 系统要求
- Payload CMS `^3.37.0`
- Next.js `^14.0.0` 或 `^15.0.0`
- React `^18.0.0` 或 `^19.0.0`
- Node.js `^18.20.2` 或 `>=20.9.0`
## 安装
```
pnpm add @inoo-ch/payload-image-optimizer
# 或
npm install @inoo-ch/payload-image-optimizer
# 或
yarn add @inoo-ch/payload-image-optimizer
```
## 快速开始
将插件添加到您的 `payload.config.ts`:
```
import { buildConfig } from 'payload'
import { imageOptimizer } from '@inoo-ch/payload-image-optimizer'
export default buildConfig({
// ...
plugins: [
imageOptimizer({
collections: {
media: true,
},
}),
],
})
```
就这样。上传到 `media` 集合的每张图片都将使用合理的默认值自动优化。
## 配置
### 完整示例
```
imageOptimizer({
collections: {
media: {
formats: [
{ format: 'webp', quality: 90 },
{ format: 'avif', quality: 75 },
],
maxDimensions: { width: 4096, height: 4096 },
},
avatars: true, // uses global defaults
},
// Global defaults (overridden by per-collection config)
formats: [
{ format: 'webp', quality: 80 },
// { format: 'avif', quality: 65 }, // opt-in — AVIF is ~5-10x slower to encode than WebP
],
maxDimensions: { width: 2560, height: 2560 },
generateThumbHash: true,
stripMetadata: true,
clientOptimization: true,
disabled: false,
})
```
### 选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| `collections` | `Record` | *必需* | 要优化的集合。使用 `true` 应用默认设置,或使用对象进行覆盖。 |
| `formats` | `FormatQuality[]` | `[{ format: 'webp', quality: 80 }]` | 输出格式和质量 (1-100)。 |
| `maxDimensions` | `{ width: number, height: number }` | `{ width: 2560, height: 2560 }` | 图片的最大尺寸。图片将被调整大小以适应这些边界。 |
| `generateThumbHash` | `boolean` | `true` | 生成 ThumbHash 模糊占位符以实现即时图片预览。 |
| `stripMetadata` | `boolean` | `true` | 从图片中移除 EXIF 和其他元数据。 |
| `uniqueFileNames` | `boolean` | `false` | 使用 UUID 替换文件名(例如,`photo.jpg` → `a1b2c3d4.webp`)。防止在上传和重新生成时出现 Vercel Blob "已存在" 错误。 |
| `clientOptimization` | `boolean` | `true` | 在上传前使用 Canvas API 在浏览器中预先调整图片大小。对于大图片,最多可减少 90% 的上传体积。 |
| `disabled` | `boolean` | `false` | 禁用优化,同时保持架构字段完整。 |
### 按集合覆盖
每个集合都可以覆盖 `formats` 和 `maxDimensions`:
```
collections: {
// Hero images: higher quality, larger dimensions
heroes: {
formats: [{ format: 'webp', quality: 95 }],
maxDimensions: { width: 3840, height: 2160 },
},
// Thumbnails: smaller, more aggressive compression
thumbnails: {
formats: [
{ format: 'webp', quality: 60 },
{ format: 'avif', quality: 45 },
],
maxDimensions: { width: 800, height: 800 },
},
}
```
### 客户端优化
当设置 `clientOptimization: true` 时,图片会在上传前在浏览器中预先调整大小。这使用 Canvas API(零额外依赖)将大图片缩小以适应 `maxDimensions`,然后再进入上传流程。
```
imageOptimizer({
clientOptimization: true,
collections: { media: true },
})
```
**它的作用:**
- 一张 12MB 的 DSLR 照片在*上传前*被调整为约 100-500KB —— 传输的数据减少 90% 以上
- 对于使用云存储 + `clientUploads: true` 的情况尤为重要,因为文件会通过 blob 存储进行往返传输
- 减少无服务器函数的处理时间(输入更小 = sharp 转换更快)
- EXIF 元数据被自动剥离(Canvas 输出不包含元数据)
**保留在服务器端的内容:** 格式转换(WebP/AVIF)、ThumbHash 生成和变体创建仍然在服务器上通过 sharp 进行,以确保质量一致。客户端仅处理调整大小 —— 这是零质量折衷的最高效优化方式。
**限制:** 仅适用于管理面板中的单文件上传。批量上传和 API/程序化上传照常在服务器端处理。
## 工作原理
1. **上传** — 图片被上传到配置的集合
2. **预处理** — 单次 sharp 流水线剥离元数据、调整大小,并可选择转换格式 —— 所有操作一次性完成
3. **保存** — Payload 将优化后的图片写入磁盘
4. **转换** — 后台作业将图片转换为额外的格式变体(例如 AVIF)并异步生成 ThumbHash
5. **完成** — 文档更新为包含变体 URL、文件大小、ThumbHash 和优化状态
格式转换和 ThumbHash 生成作为异步后台作业运行,因此上传会立即返回。
### Vercel / 无服务器部署
图片处理(尤其是 AVIF 编码和元数据剥离)可能会超过默认的无服务器函数超时时间。插件导出了一个推荐的 `maxDuration`,您可以从 Payload API 路由中重新导出:
```
// src/app/(payload)/api/[...slug]/route.ts
export { maxDuration } from '@inoo-ch/payload-image-optimizer'
```
这将超时时间设置为 60 秒,这对于大多数配置来说已经足够。如果没有此设置,繁重的处理配置可能会导致 Vercel 上的上传超时。
#### 使用 Vercel Blob 上传大文件
即使配置了 `maxDuration` 和 `bodySizeLimit`,通过 Payload 管理面板上传的大文件仍然会经过 Next.js API 路由,这会触及 Vercel 的请求体大小限制(无服务器函数上为 4.5MB)。如果您正在使用 `@payloadcms/storage-vercel-blob`,请启用 `clientUploads` 以完全绕过此限制:
```
vercelBlobStorage({
collections: { media: true },
token: process.env.BLOB_READ_WRITE_TOKEN,
clientUploads: true, // uploads go directly from browser to Vercel Blob
})
```
启用 `clientUploads: true` 后,文件直接从浏览器上传到 Vercel Blob(高达 5TB),服务器仅处理小的 JSON 元数据负载。无论文件大小如何,这都消除了请求体大小限制错误。
#### "此 blob 已存在" 错误
当 `replaceOriginal: true`(默认)时,插件会在上传期间更改文件名(例如,`photo.jpg` → `photo.webp`)。如果该名称的 blob 已存在,Vercel Blob 会抛出错误,因为 `@payloadcms/storage-vercel-blob` 不会将 [`allowOverwrite`](https://vercel.com/docs/vercel-blob#overwriting-blobs) 传递给 Vercel Blob SDK。
**修复方法:** 在插件配置中启用 `uniqueFileNames` —— 在存储适配器看到它们之前用 UUID 替换原始文件名:
```
imageOptimizer({
collections: { media: true },
uniqueFileNames: true, // photo.jpg → a1b2c3d4-5e6f-7890-abcd-ef1234567890.webp
})
```
这可以防止初始上传和批量重新生成时发生冲突(重新生成任务也会为云存储重新上传生成新的 UUID)。Payload 将完整的 URL 存储在数据库中,因此 UUID 文件名对您的应用程序是透明的。
**替代方案:** 如果您希望保留原始文件名,请在存储适配器上设置 `addRandomSuffix: true`:
```
vercelBlobStorage({
collections: { media: true },
token: process.env.BLOB_READ_WRITE_TOKEN,
clientUploads: true,
addRandomSuffix: true,
})
```
## 与 Payload 默认图片处理的区别
Payload CMS 内置了 [sharp](https://sharp.pixelplumbing.com/),可以在上传时调整图片大小并生成尺寸。此插件在 `beforeChange` 钩子中优化上传的图片,并将结果写回 `req.file.data`。Payload 的 `generateFileData` 在钩子之前运行,并使用 `Promise.all` 处理 `imageSizes` 生成,因此该插件专注于 Payload 原生不支持的功能:格式转换(WebP/AVIF)、元数据剥离和 ThumbHash 生成。使用 `clientOptimization: true`(默认)是加快包含许多 `imageSizes` 的上传的最有效方法,因为它在 Payload 处理之前减小了源图片。
### 对比
| 功能 | Payload 默认 | 使用此插件 |
|---|---|---|
| 调整至最大尺寸 | 通过 `imageSizes` 配置手动操作 | 自动 —— 全局或按集合配置一次 |
| WebP/AVIF 转换 | 需要自定义钩子 | 内置,零配置 |
| EXIF 元数据剥离 | 未内置 | 自动(可配置) |
| 模糊哈希占位符 | 需要自定义钩子 | ThumbHash 自动生成 |
| 优化状态和节省空间 | 不可用 | 每张图片的管理侧边栏面板 |
| 批量重新处理现有图片 | 不可用 | 一键重新生成,附带进度跟踪 |
| 带模糊占位符的 Next.js `` | 手动连接 | 即插即用的 `` / `` 组件 |
| 按集合覆盖格式/质量 | 不适用 | 支持 |
### CPU 和资源影响
- **单次流水线** — 元数据剥离、调整大小和格式转换在单个 sharp 流水线中运行(一次解码/编码周期),最大限度地减少了处理开销。
- **延迟 ThumbHash** — ThumbHash 生成在后台运行(通过格式转换作业或 `waitUntil`),而不是阻塞上传响应。
- **单格式模式**(例如,仅 WebP 且 `replaceOriginal: true`)与 Payload 的默认 sharp 处理相比几乎零开销 —— 插件替换了 sharp 处理过程,而不是添加第二个过程。
- **额外的格式变体**(例如 WebP 和 AVIF)在上传后作为后台作业运行 —— 这是您会看到比原生 Payload 额外 CPU 使用的唯一领域。请注意,AVIF 编码比 WebP 慢约 5-10 倍。
- **批量重新生成** 顺序处理图片,而不是一次性处理,因此不会导致服务器激增。
如果您使用的是资源受限的服务器,请使用单格式模式,您的 CPU 成本将与原生 Payload 大致相同。
## 管理 UI
插件向文档侧边栏添加了一个 **优化状态** 面板,显示:
- 状态徽章(待处理 / 处理中 / 完成 / 错误)
- 原始与优化后的文件大小及节省百分比
- ThumbHash 模糊预览缩略图
- 生成的格式变体列表,包含尺寸和文件大小
**重新生成图片** 按钮出现在集合列表视图中,允许您通过实时进度条批量重新处理现有图片。
## 显示图片
### 选项 1:`ImageBox`(新项目)
即插即用的 Next.js `` 封装 —— 以最佳实践显示图片的最简单方法:
```
import { ImageBox } from '@inoo-ch/payload-image-optimizer/client'
// Hero image — fill mode with priority
// Card grid — explicit sizes hint
// Fixed dimensions
```
**它自动执行的操作:**
- 每张图片的 ThumbHash 模糊占位符
- 平滑的模糊到清晰淡入过渡
- 来自 `focalX`/`focalY` 的焦点定位
- 响应式变体加载器 —— 直接提供预生成的 Payload 尺寸变体,而不是 `/_next/image` 重新优化(当在集合上配置了 `imageSizes` 时)
- 填充模式的智能 `sizes` 默认值 —— `(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw`,而不是浏览器的 `100vw` 假设
- 通过 `updatedAt` 进行缓存清除
### 选项 2:`getOptimizedImageProps()`(现有项目 / Payload 网站模板)
如果您正在使用 [Payload 网站模板](https://github.com/payloadcms/payload/tree/main/templates/website) 或拥有现有的 `` 组件,请添加 3 行代码:
```
import { getOptimizedImageProps } from '@inoo-ch/payload-image-optimizer/client'
const optimizedProps = getOptimizedImageProps(resource)
```
这将用每张图片的 ThumbHash 替换模板的硬编码模糊占位符,添加焦点支持,并启用响应式变体加载。
### 响应式变体加载
当您的集合配置了 `imageSizes`(例如,`thumbnail: 300`、`medium: 900`、`large: 1400`)时,`ImageBox` 和 `getOptimizedImageProps()` 都会自动创建一个混合的next/image` 加载器,它将:
1. 选择大于或等于请求宽度的最小预生成变体
2. 直接从您的存储提供它(绕过 `/_next/image` —— 无双重优化)
3. 当不存在接近的变体匹配时,回退到 `/_next/image`
这意味着上传到配置了 `imageSizes` 的集合的图片可以免费获得响应式加载 —— 无需额外配置。
## 文档架构
插件向每个配置的集合添加一个 `imageOptimizer` 字段组:
```
{
imageOptimizer: {
status: 'pending' | 'processing' | 'complete' | 'error',
originalSize: number, // bytes
optimizedSize: number, // bytes
thumbHash: string, // base64-encoded ThumbHash
error: string, // error message (if failed)
variants: [
{
format: string, // 'webp' | 'avif'
filename: string, // e.g. 'photo-optimized.webp'
filesize: number,
width: number,
height: number,
mimeType: string,
url: string,
},
],
},
}
```
## REST API 端点
### 开始批量重新生成
```
POST /api/image-optimizer/regenerate
Content-Type: application/json
{ "collectionSlug": "media", "force": false }
```
- `force: false` — 仅重新生成尚未完成的图片
- `force: true` — 从头开始重新处理所有图片
**响应:** `{ "queued": 42, "collectionSlug": "media" }`
### 检查重新生成进度
```
GET /api/image-optimizer/regenerate?collection=media
```
**响应:** `{ "collectionSlug": "media", "total": 42, "complete": 30, "errored": 1, "pending": 11 }`
两个端点都需要经过身份验证的用户。
## AI Agent 集成
面向 AI 编码代理的完整技术文档可在 [`AGENT_DOCS.md`](./AGENT_DOCS.md) 中获取。它在一个参考文件中涵盖了所有配置选项、字段架构、端点、客户端工具、后台作业和上下文标志。
### AI Agent 提示词
将此指令复制粘贴到您的 AI 编码代理,以使其自主集成插件:
## 许可证
MIT - [inoo.ch](https://inoo.ch)
标签:AVIF, CMS, EXIF, GNU通用公共许可证, MITM代理, Node.js, PayloadCMS, React, SEO, Syscalls, ThumbHash, TypeScript, WebP, 云存储, 元数据, 内容管理系统, 前端, 前端组件, 占位符, 响应式图片, 图像处理, 图片优化, 图片压缩, 图片缩放, 安全插件, 尺寸限制, 性能优化, 批量处理, 插件, 格式转换, 检测绕过, 模糊预览, 网络调试, 自动化, 自动化攻击, 隐私