WordPress 7.1 把图片处理搬到了浏览器,内存不够、服务器不支持的日子终于到头了!
WordPress 7.1 里有很多改动,但要说哪个最实在,我觉得是客户端媒体处理功能,该功能使用 WebAssembly(WASM)直接在浏览器中(而非在服务器上)完成图片压缩、尺寸调整、格式转换、旋转和缩略图生成,而且只需浏览器支持就行。
换句话说,升级到 WordPress 7.1 之后,上传图片,服务器只管存储,不用管处理了。今天就详细说一下这个功能:它到底改了什么、怎么工作的、插件和主题开发者要注意哪些地方。
传统服务端图片处理的痛
以前在 WordPress 后台上传图片时,文件会被发送到服务器,PHP(使用 GD 或 Imagick)生成缩略图(用于前端显示的各种尺寸),然后进行格式转换、处理 EXIF 旋转并缩放大尺寸图片。
这个方式受到三个东西限制:PHP 的内存限制(memory limit)、服务器 CPU 的性能以及服务器上装的图片处理库是 GD 还是 Imagick,版本多少。
如果使用的是共享主机,内存不够不能调 memory_limit,CPU 是共享的,有没有装 GD 或 Imagick、是否已经支持 AVIF 和 HEIC 转换,这些都不是自己能决定,也不是装个插件就能解决的。即使使用的是 VPS 服务器,对服务器不熟悉的话,自己去改配置、装组件,也是一件头疼的事情。而且同一个主机商,GD 和 Imagick 处理之后的图片质量也不一样,你本地看着挺好,换个服务器就变样,难绷!
这也是 WordPress 之前被人经常诟病的一点,上传大图经常失败。是的,图片一大,PHP 内存直接爆,或者脚本超时,就看到一个转圈转到死的进度条。
什么是客户端媒体处理?
随着浏览器能力的迭代,对图片也从只能简单浏览变得能够进行复杂处理。所以 WordPress 7.1 就把图片处理全部挪到了浏览器端,当然服务端相关功能还保留着,如果用户的浏览器不支持的时候,自动回退到服务端处理。
客户端媒体处理在浏览器端用的是 wasm-vips,它是高性能图片处理库 libvips 的 WebAssembly 编译版。图片(包括所有缩略图)在浏览器里处理完之后,再上传存到服务器上。

在客户端所有操作完成后,最后一步确认的时候会触发上下文为 'update' 的 wp_generate_attachment_metadata 钩子,让插件能够拿到全部尺寸的缩略图的元数据,这与服务器图片上传时候的方式一致(服务端生成缩略图时同样会触发 'update' 阶段)。
这样做有什么好处?
- 图片输出高质量且一致,并支持现代格式:
无论服务器安装的是 GD 还是 Imagick,也不管什么版本,所有用户获得的都是 libvips 处理的,图片高质量且一致。 - 访客下载更快:
libvips 的压缩效果比 GD 和 Imagick 都好(JPEG 用的是类 MozJPEG 的编码,体积能小 15% 左右),所以最后访客看到图更小,加载也更快。 - PHP 内存溢出错误不再出现:
之前大图处理会时不时报超出 PHP 内存限制错误,现在跑在浏览器的内存空间里,多大的图都能过,不再受内存限制。 - 服务器负载减轻:
图片处理工作被分担到用户设备处理,服务器的 CPU 和内存腾出来干别的任务。 - iPhone 照片直接传:
HEIC 在浏览器里解码,转成 JPEG 再上传,你的主机哪怕完全没有服务端 HEIC 支持也无所谓。原始 HEIC 会作为source_image附属文件留着,删附件时一起删。 - 无需支持也能上传 AVIF:
客户端处理开启后,即使主机的 PHP 图片编辑器不支持 AVIF,也照样能接受 AVIF 上传。在客户端解码的上传,MIME 类型检查会被跳过。 - 不透明动态 GIF 转成视频:
不透明的动态 GIF 在浏览器中转成 MP4/WebM 视频,播放效果与原 GIF 一模一样,不仅体积更小,而且自动播放和循环也都保留。 - 上传更可靠:
每个尺寸的缩略图上传都是独立请求,网络抽风不会整批报废;失败的请求会按指数退避自动重试(间隔越来越长),偶发的网络错误无需人工干预会自动恢复;离线时上传暂停,网络恢复后会自动续传。
具体都做了哪些事?
- 图片处理:
在 Web Worker 里用 WebAssembly 做压缩、调整大小、裁剪、格式转换(JPEG、PNG、WebP、AVIF、GIF)、EXIF 旋转,以及渐进式或交错式编码。 - 缩略图生成:
所有已注册尺寸的缩略图都在客户端生成,再通过新的 sideload REST API 接口一个一个传上去(sideload 的意思,是让服务器自己把文件收下来、登记进媒体库)。跟内置尺寸撞了参数的自定义尺寸(比如 Twenty Eleven 的large和medium_large其实一样)会去重成一个物理文件,然后注册到所有匹配的尺寸名下。 - HEIC/HEIF 支持:
iPhone 的照片(image/heic、image/heif)在浏览器里解码,转成适合网页用的 JPEG 再上传,原始 HEIC 会作为附属文件(source_image)留着,删附件时一起删。 - AVIF 支持:
AVIF 可在客户端解码,不再需要服务器端支持,高位深 AVIF 源文件(10 位或 12 位,常见于 HDR 照片)在生成的缩略图中仍会保留其位深。 - Gain Map HDR 支持:
UltraHDR JPEG 在标准的 SDR 基础图里嵌了一张增益图(Gain Map),这是 Google(UltraHDR)、Apple(Adaptive HDR)和 Adobe(Camera Raw、Lightroom、Photoshop)都支持的一种向后兼容的 HDR 方案。这类文件上传时会被识别出来并全程保留:原始文件原样上传,生成的每个缩略图也都会保留增益图,所以缩略图一样是 HDR。为了避免换编码器导致增益图丢失,进行格式转换时image_editor_output_format钩子会自动跳过这类文件。 - 自动格式转换:
客户端处理进行格式转换会遵循现有的image_editor_output_format钩子,并且支持自动转换(如 JPEG 转 WebP)。 - 动态 GIF 转视频:
不透明的动态 GIF 会用原生 WebCodecs API 和 mediabunny 库,在浏览器里转成 MP4(或 WebM)。GIF 在媒体库里仍然是一个image/gif附件,转出来的视频和首帧封面图作为附属文件旁载,记录在media_details.animated_video和animated_video_poster里。编辑器里可以把区块切换成视频区块的「GIF」变体(自动播放、循环、静音,播放效果跟原 GIF 一样),前端渲染出来的是原生<video>标签。这个转换完全可逆,透明的 GIF 还是图片,不支持 WebCodecs 视频编码的浏览器(比如 Firefox)会直接上传原始 GIF。 - 编辑器开启跨源隔离:
要跑 WASM 这一套,编辑器需要SharedArrayBuffer,而浏览器只给跨源隔离(Cross-Origin Isolated)的文档开这个口子。WordPress 的做法是在区块编辑器页面上发一个Document-Isolation-Policy: isolate-and-credentialless响应头,但是需要 Chromium 137+。除了媒体处理,这还带来一个附带好处:SharedArrayBuffer和高精度计时器现在对编辑器里跑的任何代码都可用,插件作者可以借此做多线程或者基于 WASM 的功能。因为 DIP 是按文档生效的,所以不需要像 COOP/COEP 那样给整个页面套上限制。 - 服务器端 Hook 照样触发:
wp_generate_attachment_metadata的触发逻辑与服务器端上传时一样,初始上传时以'create'上下文触发一次,在POST /wp/v2/media/{id}/finalize之后以'update'上下文再次触发。挂载该钩子的插件(如水印、CDN 同步等)无需修改即可继续工作。 - 上传进度反馈:
编辑器底部的提示条会跟踪批量上传的进度,上传时显示转圈,完成后短暂显示一个对勾。客户端和服务端两条路径都适用,状态还会通过wp.a11y.speak()播报给用屏幕阅读器的用户。 - 自动降级:
浏览器不支持必要特性时,自动退回服务端处理,用户侧无感知。 - 质量设置照旧:
标准的wp_editor_set_quality和jpeg_quality钩子,会通过上传响应里按尺寸算出来的image_quality字段传给客户端生成子尺寸的逻辑,你原来调质量的代码可以直接接着用。 - 外部图片交给服务器端导入:
图片区块的「上传到媒体库」操作,以及发布前的「外部媒体」面板,现在会把图片 URL 发给服务器,由服务器下载并旁载,彻底绕开浏览器的 CORS 问题。
详细的技术细节

首先是客户端,WordPress将其分成四层:
@wordpress/vips - 在 Web Worker 中封装 wasm-vips 实现非阻塞图片处理。WASM 包是懒加载的,第一次用到才下载,打包了 vips.wasm 和 vips-heif.wasm(后者专门用来解码 AVIF)。
@wordpress/upload-media – 管理上传队列和并发(最多 5 个上传任务,最多 2 个图片处理任务),并协调整个流水线。
@wordpress/media-utils – 负责跟 WordPress REST API 打交道的 HTTP 传输。
@wordpress/video-conversion – 在 Web Worker 里封装 mediabunny 库(照着 @wordpress/vips 的模式),在后台线程里把动态 GIF 转成 MP4/WebM。这个功能受 WebCodecs ImageDecoder 和 VideoEncoder 的可用性限制,并发上限是 1。
再看 PHP 端:
功能开关:WordPress 提供了 wp_is_client_side_media_processing_enabled() 函数来判断功能是否开启,还有一个 wp_client_side_media_processing_enabled 钩子可以开关它。
跨源隔离:客户端媒体处理开启时,WordPress 会在 load-post.php、load-post-new.php、load-site-editor.php、load-widgets.php 这几个页面上,通过 wp_start_cross_origin_isolation_output_buffer() 发出 Document-Isolation-Policy 响应头,且只对 Chromium 137+ 的浏览器生效。
REST API 扩展:新增了 generate_sub_sizes 和 convert_format 两个参数,旁加载接口(POST /wp/v2/media/{id}/sideload),最终确认接口(POST /wp/v2/media/{id}/finalize),HEIC 附属文件上传用的 replace_file 标志,还有几个新的响应字段(exif_orientation、missing_image_sizes、filename、filesize)。
想看完整架构的话,官方有一份客户端媒体处理架构文档。
插件开发者要注意什么
想关掉客户端处理
用 wp_client_side_media_processing_enabled 钩子就行:
add_filter( 'wp_client_side_media_processing_enabled', '__return_false' );
服务端钩子还会不会触发
大家最关心的问题来了:客户端处理绕过了服务端生成图片,那挂在 wp_generate_attachment_metadata 上的插件会不会失效?答案是不会的,这个钩子的触发方式跟服务端上传完全一致,只是拆成了两个阶段:初始上传时(生成缩略图之前)触发一次,上下文是 'create';等客户端所有缩略图旁载完成后,最终确认接口运行的时候再触发一次,上下文是 'update'。
做水印、CDN 同步、自定义元数据的插件不用改就能继续跑,但要确保自己的逻辑是幂等的(同一段代码跑两次结果一样),能正确处理触发两次的情况。
还有一点,如果确认那一步失败了,系统只会记一条日志,上传本身仍然算成功。这步属于「尽力而为」,插件出问题不会拦住用户上传。
现有的钩子依然生效
客户端处理会读取服务器设置,这几项配置照常起作用:
big_image_size_threshold– 缩放前的最大尺寸阈值image_editor_output_format– 自动格式转换image_save_progressive– 渐进式或交错式编码wp_image_maybe_exif_rotate– EXIF 旋转wp_editor_set_quality(以及针对 JPEG 输出的jpeg_quality)– 按每个已注册尺寸解析出来的编码质量
注意,这里没有 client_side_supported_mime_types 钩子。支持的类型写死在 CLIENT_SIDE_SUPPORTED_MIME_TYPES 里,就这几种:image/jpeg、image/png、image/gif、image/webp、image/avif。
怎么控制图片质量
客户端编码走的是跟服务端一样的 PHP 钩子:wp_editor_set_quality 以及针对 JPEG 的 jpeg_quality。服务端按注册尺寸把钩子的结果算出来,放在上传响应的 image_quality 字段里,客户端在调整尺寸和转码时应用该值,用于调整服务器端质量的代码可直接复用:
/*
* 感知尺寸调整:将宽度在 300px 及以下的 JPEG 缩略图质量降至 60,其余较大尺寸保持不变。
*/
add_filter(
'wp_editor_set_quality',
function ( $quality, $mime_type, $size ) {
if ( 'image/jpeg' === $mime_type && isset( $size['width'] ) && $size['width'] <= 300 ) {
return 60;
}
return $quality;
},
10,
3
);
当服务器没有返回该字段时,客户端默认用 0.82。
跨源隔离的影响
客户端媒体处理启用后,WordPress 会在 Chromium 137+ 的区块编辑器页面上发出 Document-Isolation-Policy: isolate-and-credentialless。因为 DIP 是按文档生效的,它不会像 COEP/COOP 那样给整个页面套上限制。有三点要注意:
- 跨源加载的外部脚本会自动加上
crossorigin="anonymous"属性,服务端靠wp_add_crossorigin_attributes()的输出缓冲,客户端靠 MutationObserver。<img>被排除在外,所以外部图片的预览不受影响。 action非edit的后台页面上会跳过 DIP,这样依赖同源 iframe 访问的第三方页面构建器还能正常工作。- 外部图片交给服务端导入:「上传到媒体库」和发布前的「外部媒体」面板,会把图片 URL POST 给媒体接口(新增的
url参数),由服务器下载并旁载。导入远程媒体的插件也应该照这个做法来,别在浏览器里用fetch()去取图片字节,跨源 fetch 受 CORS 限制,在credentialless隔离文档里会直接失败。
内容安全策略 (CSP)
如果您的插件设置了 CSP,请确保 worker-src 指令包含 blob::
Content-Security-Policy: worker-src 'self' blob:;
少了这一项,WASM 处理用的 Worker 建不起来,整条流程会退回服务端处理。
服务端专属的钩子不会触发了
因为不再走服务端图片编辑器(WordPress 里负责改图的那个 WP_Image_Editor 类),wp_image_editors、image_memory_limit 和 image_make_intermediate_size 这三个钩子不会被触发。这是这次改动里最容易踩的坑,挂在这上面的插件逻辑会直接失效,手上有相关插件的赶紧去查一遍。
主题开发者:基本不用管
客户端媒体处理对主题是透明的,big_image_size_threshold、image_editor_output_format 这些现有钩子不用改就能继续用。
用 add_image_size() 注册的尺寸会自动在客户端生成缩略图,跟内置尺寸重合的会自动去重成一个文件。
浏览器兼容性与自动回退
客户端处理依赖 Document-Isolation-Policy 来开启 SharedArrayBuffer,目前该特性仅在基于 Chromium 的浏览器中可用。
| 浏览器 | 最低版本 | 状态 |
| Chrome | 137+ | 通过 Document-Isolation-Policy 提供完整支持 |
| Edge | 137+ | 通过 Document-Isolation-Policy 提供完整支持 |
| Firefox | – | *不支持(缺乏 Document-Isolation-Policy) 自动降级至服务器端处理 |
| Safari | – | *不支持(缺乏 Document-Isolation-Policy) 自动降级至服务器端处理。 浏览器内 HEIC 解码依然有效,因其不需要 Document-Isolation-Policy。 |
Chrome 和 Edge 从 137 版本(2025 年年中发布)开始支持 Document-Isolation-Policy,绝大多数 Chromium 用户都已经满足要求。安卓版 Chrome 从 146 版本开始支持。
- 对于不支持的浏览器,WordPress 会自动降级到服务器端处理,用户体验无差异。
- 社区已经有插件(Client-Side Media Everywhere)能在 Firefox 和 Safari 上开启客户端媒体处理,原理是改发 COEP/COOP 响应头。核心没默认这么做,是因为这套头可能跟嵌入组件这类第三方资源产生兼容性问题,因此默认未采用。
开启前还要过几道检测
除了浏览器支持,客户端在启动 WASM 流水线之前还会检查几项运行时条件。任何一项不过,都会无缝退回服务端处理:
| 检查项 | 阈值 | 原因 |
| 设备内存 | > 2 GB | 内存太小的设备上跑 WASM 图片处理,可能直接 OOM(内存溢出) |
| CPU 核心数 | ≥ 2 | WASM 图片处理至少需要一个核心给 Worker,一个核心给 UI 线程。 |
| 网络状况 | 非 2g / slow-2g且无 Save-Data 标头 | Worker 有大约 13 MB 要下载,只让较快的网络走这条路,3g 是允许的 |
CSP blob: Workers | 必须成功 | Worker 是从 blob URL 创建的,严格的 worker-src 策略会拦截它。 |
几个常见问题
1. 只是省带宽吗?
不是。客户端要上传原图,外加每一个生成的缩略图,所以上传阶段传的总字节数其实是变多的(服务端处理则仅接收原图)。带宽的节省体现在访客那一侧:编码更好,现代格式永远支持。
对上传过程来说,真正的收益是给服务器的 CPU 和内存减负,主机不用再为生成子尺寸付出 GD/Imagick 的计算成本,而这正是共享主机上 PHP 超时和内存溢出最常见的原因之一。
2. 「永远不要信任客户端」还适用吗?
客户端处理属于性能优化,而非安全信任边界。
服务器照样会对每一个上传的文件做校验,MIME 类型、图片尺寸、权限检查、安全过滤一个不少,wp_generate_attachment_metadata 的钩子链也照常跑。浏览器处理不了或者拒绝处理的文件,WordPress 会无缝退回服务端处理。
3. 浏览器处理不了图片会怎么样?
跟以前一样走服务端处理。降级是自动的,用户感觉不到,界面没变化,也不报错。
4. 挂载在 wp_generate_attachment_metadata 钩子的插件还会运行吗?
会。
触发逻辑跟服务端上传一致:初始上传时以 'create' 上下文触发一次,旁载完成后通过 finalize 接口以 'update' 上下文再触发一次。做水印、CDN 同步、自定义元数据的插件不用改,但要检查一下能不能正确处理触发两次的情况。
5. 这会改变用户上传的格式吗?
只有在 image_editor_output_format 指定了转换时才会改。另外有两个新行为:HEIC 输入会在上传前转成 JPEG,原图作为附属文件保留;不透明的动态 GIF 会被转成附属视频。即使主机没有服务端 AVIF 支持,AVIF 输入也能原样作为 AVIF 上传。带增益图的 HDR 图片会原样上传,生成的每个子尺寸都保留 HDR 增益图。
6. 动态 GIF 会有什么变化?
不透明的动态 GIF 会在浏览器里转成附属的 MP4/WebM 视频,编辑器里可以把它切换成视频区块的「GIF」变体(自动播放、循环、静音),几个关键细节:
- 附件类型还是 GIF:媒体库里它仍然是一个
image/gif附件,视频和首帧封面图作为附属文件记录在media_details.animated_video和animated_video_poster里,删 GIF 的时候会一起清掉。 - 前端渲染成原生视频区块:编辑器里是区块切换,发布出去的 HTML 是原生的
<video autoplay loop muted playsinline poster>标签。 - 转换完全可逆,透明 GIF 保持图片形式,而且只有独立的图片区块会转(画廊、媒体与文本、封面区块里的 GIF 不受影响)。
- 浏览器支持:转换需要 WebCodecs 视频编码能力,不支持的浏览器(比如 Firefox)会直接上传原始 GIF,不会报错。
7. 为什么 Firefox 和 Safari 不受支持?
因为它们还没有提供 Document-Isolation-Policy,而 WASM 流水线需要的 SharedArrayBuffer 得靠它才能启用。这两个浏览器上的用户会走原来的服务端处理路径,体验不会变差。Safari 上的 HEIC 画布降级解码依然有效。
最后
文章比较长,官方相关的文档在这,想深度了解的可以看下:
官方原文:
https://make.wordpress.org/core/2026/07/22/client-side-media-processing-in-wordpress-7-1/
开发者指南:
https://developer.wordpress.org/block-editor/how-to-guides/client-side-media/
