每个写过 Dockerfile 的人都有过这样的体验——docker pull python:3.12-slim,144MB 进来了;再装几个依赖,轻松突破 280MB。你只是想跑一个几百行的小服务,却要先搬运一个比整个 3D 游戏引擎还大八倍的运行环境。
开发者 Sergey Bogomolov 最近做了一件事:把一个完整的 3D 游戏引擎导出为 WebAssembly,结果整个二进制文件只有 35MB。不需要安装,不需要容器运行时,任何现代浏览器打开就能跑。这个数字背后不只是"压缩得好",而是两种完全不同的交付哲学在碰撞。
Docker 镜像为什么这么胖
Docker 镜像的体积问题不是某个发行版的锅,而是打包方式的系统性冗余:
- 完整 POSIX 用户空间:即使 slim 镜像也带着 apt、dpkg、shell、coreutils 等你大概率不会直接调用的工具链。
- 动态链接与共享库:glibc、libssl、libz 等基础库必须打包,哪怕你的程序只用到了其中一小部分符号。
- 层叠加不可去重:每一层是完整快照,删文件只是标记删除,体积不减。多阶段构建能缓解,但底层镜像的冗余依然存在。
一个 python:3.12-slim 镜像里,真正被你的业务代码调用的二进制内容可能不到 20MB,剩下的 120MB 是"以防万一"的运行时基础设施。
WASM 的 35MB 是怎么做到的
游戏引擎包含渲染管线、物理模拟、音频处理、脚本绑定——功能密度远超一个 Python 运行时。35MB 能装下这一切,核心原因有三:
1. 编译期极致裁剪。WASM 工具链(如 Emscripten、wasm-pack)在链接阶段只保留被实际引用的函数和数据段。没有"装了但没用"的共享库,没有为了兼容性而保留的旧符号。
2. 无操作系统依赖。WASM 不需要 glibc、不需要 systemd、不需要包管理器。它运行在浏览器或 WASI runtime 提供的极薄抽象层上,二进制里只有你的业务逻辑。
3. 字节码设计紧凑。WASM 的指令编码是二进制栈机格式,比 x86 机器码更密集;加上 LEB128 变长编码,常见操作只占 1-2 字节。35MB 的 WASM 在功能上可能等价于 80-100MB 的原生 ELF 二进制。
动手对比:一个最小 HTTP 服务的两种交付
下面用同一个功能——"返回当前时间的 HTTP 服务"——分别用 Docker 镜像和 WASM 交付,直观感受体积差异。
Docker 方案(Python)
# Dockerfile
FROM python:3.12-slim
COPY server.py /app/server.py
WORKDIR /app
CMD ["python", "server.py"]
# server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = datetime.now().isoformat().encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
构建并查看体积:
docker build -t time-server .
docker images time-server
# REPOSITORY TAG IMAGE ID SIZE
# time-server latest xxxxx ~280MB
WASM 方案(Rust + Wasmtime)
// src/main.rs
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
println!("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n{}", secs);
}
# Cargo.toml
[package]
name = "time-server"
version = "0.1.0"
edition = "2021"
[dependencies]
wasi = "0.12"
编译为 WASM 并检查体积:
# 安装 wasm32 target(如果还没有)
rustup target add wasm32-wasi
# 编译
cargo build --target wasm32-wasi --release
# 查看体积
ls -lh target/wasm32-wasi/release/time_server.wasm
# -rw-r--r-- 1 user staff ~0.8MB time_server.wasm
# 用 Wasmtime 运行(无需容器运行时)
wasmtime target/wasm32-wasi/release/time_server.wasm
同一个"返回时间"的功能:Docker 镜像 280MB,WASM 二进制 0.8MB。差距不是优化技巧带来的,是交付模型本身决定的。
注意:上面的 Rust 示例输出的是原始文本,不是真正的 HTTP 服务器。如果需要 WASM 内跑 HTTP 服务,可以基于
wasi-httpproposal 或 Spin / Fermyon 等框架实现,体积仍然在几 MB 级别。
什么时候该认真考虑 WASM 交付
WASM 不是要取代 Docker,而是在特定场景下提供了更轻的替代路径:
| 场景 | Docker 更合适 | WASM 更合适 |
|---|---|---|
| 需要完整 Linux 用户空间 | ✅ | ❌ |
| 长运行服务 + 复杂进程管理 | ✅ | ⚠️ 生态还在成熟 |
| 插件 / 扩展 / 边缘函数 | ❌ 过重 | ✅ 毫秒级冷启动 |
| 跨平台客户端渲染(浏览器内) | ❌ | ✅ 天然支持 |
| 多语言沙箱隔离 | ⚠️ 需配置 | ✅ 线性内存天然隔离 |
务实建议:
- 边缘和插件场景优先试水。WASM 的冷启动时间在微秒级,比容器快 100 倍以上,适合函数式计算和插件架构。
- 不要为了体积强行迁移。如果你的服务已经稳定运行在容器里,没有体积痛点,迁移收益有限。
- 关注 WASI 进展。WASI Phase 2 正在标准化网络、文件系统、线程等系统接口,一旦落地,WASM 在服务端的适用范围会大幅扩展。
- 混合交付。核心服务用容器,边缘函数和浏览器端模块用 WASM——两者可以共存,不必二选一。
35MB 装下整个游戏引擎这个事实,最大的意义不是"WASM 能压缩",而是提醒我们重新审视交付物的必要性:你搬运的那 280MB 里,有多少是真正在运行的代码?