Figma 从 WebGL 到 WebGPU:一场浏览器图形引擎的工业级迁移实录
前言:当设计工具开始"重新投胎"
2026年,全球最受欢迎的设计工具 Figma 完成了其历史上最大规模的技术架构迁移——将核心渲染引擎从 WebGL 全面升级至 WebGPU。这不是一次简单的 API 版本升级,而是一场涉及数十亿行代码、数百万用户交互的工程壮举。
如果你以为这只是"换个图形库",那就太天真了。Figma 的渲染引擎每天要处理来自全球数千万用户的复杂设计文件,涉及矢量图形、位图渲染、文本排版、实时协作同步——任意一个环节出问题,都意味着设计工作流的灾难性崩溃。
本文将深入剖析这次迁移的技术内幕,从架构设计、Shader 语言转换、缓冲区管理到兼容性回退机制,完整还原一个顶级工程团队如何在不停服的情况下,完成了一次"外科手术式"的引擎替换。
一、为什么 WebGL 到了天花板
1.1 WebGL 的历史功绩与时代局限
WebGL 诞生于 2011 年,是一种基于 OpenGL ES 2.0 的浏览器图形 API。它的出现让 Web 应用第一次具备了接近原生水平的 3D 图形渲染能力。在过去的十五年里,WebGL 支撑了无数令人惊叹的 Web 应用——从 Three.js 驱动的 3D 可视化到复杂的在线设计工具。
但到了 2026 年,WebGL 的局限性已经严重制约了图形应用的上限:
线程模型瓶颈:WebGL 所有命令都在主线程执行,JavaScript 与 GPU 命令之间的往返开销巨大。当场景复杂度上升时,主线程阻塞导致的帧率下降是 WebGL 应用挥之不去的痛。
API 设计老旧:WebGL 基于 OpenGL ES 2.0 设计,很多现代 GPU 特性(如 compute shader、纹理数组、直接缓冲区访问)根本不支持。开发者只能通过 hack 方式模拟,效率低下且代码丑陋。
内存管理粗放:WebGL 的内存管理依赖浏览器自动回收,缺乏细粒度控制。大量纹理的创建与销毁会造成显存碎片化,影响长期运行的稳定性。
多线程协作困难:Worker 线程无法直接操作 WebGL 上下文,跨线程的渲染状态同步只能靠消息传递,性能损耗显著。
1.2 Figma 的具体痛点
Figma 作为一款实时协作设计工具,其渲染压力远高于普通图形应用:
- 矢量图形渲染:设计师可能在画布上放置数万个矢量节点,每个节点涉及路径计算、填充、描边
- 实时协作同步:多个用户同时编辑时,所有变更需要实时同步与渲染,延迟要求极高
- 超大幅面设计:一个 8K 广告设计文件可能有数千个图层,每个图层独立渲染
- 帧率稳定性:60fps 是 Figma 的底线,任何卡顿都会直接打断设计师的创作心流
在 WebGL 架构下,Figma 团队为了解决上述问题,不得不在 JavaScript 层实现了大量优化策略——对象池、着色器缓存、脏矩形渲染等。但这些优化本质上是在和 WebGL 的架构缺陷搏斗,到了某个临界点,优化收益趋向于零。
WebGPU 的出现改变了这一切。
二、WebGPU:重新定义浏览器图形能力
2.1 架构层面的革命性变化
WebGPU 是 W3C 于 2023 年正式标准化的新一代浏览器图形 API。与 WebGL 的"修修补补"不同,WebGPU 从架构层面进行了重新设计:
WebGL 架构:
[Javascript] → [命令队列] → [浏览器GPU代理] → [OpenGL ES] → [GPU驱动] → [GPU]
↑ 主线程瓶颈(所有渲染都在主线程)
WebGPU 架构:
[Javascript/Worker] → [GPU Command Encoder] → [WebGPU Device] → [Native GPU API] → [GPU]
↓ ↓
多线程并行编码 异步命令提交
核心差异:
- 命令编码器(Command Encoder):渲染命令先编码为二进制缓冲区,再异步提交到 GPU,实现主线程与非关键路径的解耦
- 计算着色器(Compute Shader):不仅支持传统的顶点和片元着色器,还能执行通用 GPGPU 计算
- 现代 GPU 特性原生支持:纹理数组、存储缓冲区、无绑定点布局……这些在 WebGL 中需要 hack 实现的功能,WebGPU 原生支持
- 精细化内存管理:显式的资源生命周期管理,开发者完全掌控显存分配
2.2 WGSL:WebGPU 的着色器语言
WebGPU 使用全新的着色器语言 WGSL(WebGPU Shading Language),取代了 WebGL 中沿用的 GLSL(OpenGL Shading Language)。两者有着本质区别:
WGSL 示例(顶点着色器):
// 声明uniform绑定组(相当于WebGL的uniform buffer object)
@group(0) @binding(0) var<uniform> Uniforms: UniformsBlock;
// 输出结构体
struct VertexOutput {
@builtin(position) Position: vec4<f32>,
@location(0) color: vec4<f32>,
};
// 入口函数:顶点着色器
fn main(
@builtin(vertex_index) VertexIndex: u32,
) -> VertexOutput {
var output: VertexOutput;
// 三角形顶点数据(3个顶点 x 2个分量 = 6个浮点数)
let pos = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>(0.5, -0.5)
);
// 颜色数据
let colors = array<vec4<f32>, 3>(
vec4<f32>(1.0, 0.0, 0.0, 1.0),
vec4<f32>(0.0, 1.0, 0.0, 1.0),
vec4<f32>(0.0, 0.0, 1.0, 1.0)
);
output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
output.color = colors[VertexIndex];
return output;
}
对应 GLSL(WebGL)版本:
// 精度声明(WGSL默认精度更高)
attribute vec2 a_position;
attribute vec4 a_color;
varying vec4 v_color;
uniform mat4 u_modelViewProjection;
void main() {
gl_Position = u_modelViewProjection * vec4(a_position, 0.0, 1.0);
v_color = a_color;
}
从对比可以看出:
| 特性 | WGSL | GLSL |
|---|---|---|
| 顶点索引 | @builtin(vertex_index) | gl_VertexID(隐式) |
| 输出变量 | struct + @location | varying |
| uniform | var<uniform> 强类型绑定 | uniform 松散声明 |
| 函数参数 | 显式 @param 装饰器 | 隐式参数传递 |
| 数值类型 | f32、u32、i32 明确 | float、int 模糊 |
WGSL 的类型系统更严格,编译器能在编译期发现更多错误,减少运行时崩溃。但对于习惯了 GLSL 的开发者来说,这套新的语法体系需要重新学习曲线。
三、Figma 的迁移策略:分阶段平滑过渡
3.1 为什么不能一次性迁移
Figma 的用户规模决定了它不可能走"大爆炸"式的迁移路径——一夜之间将数千万用户切换到新引擎,一旦出现兼容性问题,后果不堪设想。
Figma 团队采用了三层回退机制:
Layer 3: 用户级别回退(最顶层)
↓ 如果 WebGPU 完全不可用
Layer 2: 功能级别回退(中等)
↓ 如果 WebGPU 功能不全
Layer 1: WebGL 兜底(最底层)
Layer 1 - WebGL 兜底:保留完整的 WebGL 渲染路径,作为最终保底。所有 WebGPU 无法处理的场景,自动降级到 WebGL。
Layer 2 - 功能级别回退:即使设备支持 WebGPU,如果某些特性(如 compute shader)不可用,相关功能模块切换到 WebGL 版本。
Layer 3 - 用户级别回退:提供用户控制开关,允许用户在设置中强制使用 WebGL 或 WebGPU。
3.2 动态检测机制
Figma 实现了精确的 WebGPU 可用性检测:
// WebGPU 可用性检测核心逻辑
async function detectWebGPUSupport(): Promise<WebGPULevel> {
// Level 0: 完全不支持
if (!navigator.gpu) {
return 'webgl'; // 降级到 WebGL
}
// Level 1: 基础 WebGPU 支持
const adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance'
});
if (!adapter) {
return 'webgl';
}
// Level 2: 检查必要特性
const device = await adapter.requestDevice({
requiredFeatures: ['float32filterable']
});
if (!device) {
return 'webgl';
}
// Level 3: 完整 WebGPU(包含 compute shader)
const features = device.features;
const hasComputeShader = features.has('compute-variable-sized-loads');
const hasStorageTexture = features.has('chromium-experimental-subgroups');
return hasComputeShader && hasStorageTexture ? 'full-webgpu' : 'partial-webgpu';
}
这个检测机制在 Figma 启动时运行,决定用户会话使用哪套渲染路径。实际数据显示,全球约 85% 的用户设备能支持 Level 2 以上,剩下 15% 自动降级到 WebGL——其中以旧款 macOS 设备和老旧集成显卡为主。
四、Shader 迁移:GLSL 到 WGSL 的自动转换
4.1 为什么需要自动转换
Figma 的 WebGL 版本积累了数百个着色器,覆盖了从简单形状渲染到复杂滤镜处理的全部图形功能。这些着色器经过了多年的优化与调试,代码量巨大且高度定制化。
如果完全手动翻译这些 GLSL 着色器为 WGSL:工作量大不说,引入的错误可能比解决的问题还多。Figma 团队开发了一个专门的着色器处理器(Shader Processor),用于自动完成 GLSL → WGSL 的转换。
4.2 转换器的架构设计
[原始 GLSL 着色器]
↓
[词法分析器 (Lexer)] → Token 流
↓
[语法分析器 (Parser)] → AST (抽象语法树)
↓
[GLSL 语义分析器] → 中间表示 IR
↓
[WGSL 代码生成器] → 目标 WGSL 代码
↓
[验证器 (WGSL Validator)] → 通过/失败
核心难点在于 GLSL 与 WGSL 语义差异的转换。以下是 Figma 着色器处理器的核心转换规则:
4.2.1 Uniform 声明转换
GLSL 写法:
uniform mat4 u_modelMatrix;
uniform vec3 u_lightPosition;
uniform float u_opacity;
uniform sampler2D u_texture;
WGSL 转换后:
struct UniformsBlock {
modelMatrix: mat4x4<f32>,
lightPosition: vec3<f32>,
opacity: f32,
};
// 不同类型的 uniform 放在不同的绑定组
@group(0) @binding(0) var<uniform> Uniforms: UniformsBlock;
@group(0) @binding(1) var texture_1: texture_2d<f32>;
@group(0) @binding(2) var sampler_1: sampler;
这里的关键变化:WGSL 要求 uniform 数据必须打包成结构体,且不同类型资源放在不同绑定组(binding group)。GLSL 中的松散 uniform 声明在 WGSL 中被强制结构化。
4.2.2 内置函数映射
GLSL 与 WGSL 的内置函数名称有不少差异:
| GLSL 函数 | WGSL 等价 |
|---|---|
gl_FragColor | @location(0) 输出到颜色附件 |
texture2D() | textureSample() |
textureLod() | textureSampleLevel() |
normalize() | normalize() (相同) |
mix() | mix() (相同) |
reflect() | reflect() (相同) |
fract() | fract() (相同) |
atan(y, x) | atan2(y, x) |
mod() | mod() (相同,但参数顺序相反!) |
inversesqrt() | inverseSqrt() (小写转驼峰) |
length() | length() (支持向量泛型) |
4.2.3 精度声明转换
GLSL 的精度声明需要特殊处理:
GLSL:
precision mediump float;
attribute highp vec2 a_position;
varying lowp vec4 v_color;
WGSL:
// WGSL 默认精度就是 f32,不需要显式声明
// 但可以显式指定
fn fragmentShader(
@location(0) color: vec4<f32>,
) -> @location(0) vec4<f32> {
// 精度由类型决定:f32 高精度,i32/u32 整数精度
return color;
}
WGSL 不使用 lowp/mediump/highp 三级精度体系,而是通过数据类型隐式决定精度——f32 即高精度,没有性能惩罚的额外顾虑。
4.2.4 类型差异的自动修复
GLSL 和 WGSL 在数值类型上存在微妙的差异,最容易出问题的是向量索引操作:
GLSL(允许运行时常量向量元素访问):
vec4 color = vec4(1.0, 0.0, 0.0, 1.0);
float r = color[0]; // 合法,但行为依赖实现
WGSL(必须使用显式 .x / .r 访问器):
var color: vec4<f32> = vec4<f32>(1.0, 0.0, 0.0, 1.0);
// color[0] 在 WGSL 中不合法
let r: f32 = color.x; // 或 color.r(等价)
着色器处理器需要检测所有向量索引访问,将其转换为 WGSL 合法的访问器表达式。Figma 的处理方案是构建完整映射表:0→x/r, 1→y/g, 2→z/b, 3→w/a。
五、缓冲区管理:从粗放到精细
5.1 WebGL 时代的缓冲区困境
在 WebGL 架构下,Figma 的缓冲区管理存在几个根深蒂固的问题:
问题一:缓冲区频繁重建
设计师切换页面或缩放画布时,大量顶点缓冲区被创建、填充、销毁。每秒可能有数千次这样的操作,导致 GPU 显存碎片化。
问题二:状态切换开销
WebGL 的渲染状态(绑定 program、绑定 buffer、设置 viewport 等)分散在各处,频繁的状态切换需要昂贵的 GPU 驱动调用。
问题三:多纹理绑定效率低
Figma 设计文件中经常有数十个独立纹理(图标、图片、背景等)。WebGL 的纹理切换开销巨大,限制了多图层场景的渲染性能。
5.2 WebGPU 的统一缓冲区设计
WebGPU 引入的**统一缓冲区(Uniform Buffer)和绑定组(Bind Group)**机制彻底改变了这一局面:
// 创建统一缓冲区(存储所有 uniforms)
const uniformBuffer = device.createBuffer({
size: 256, // 对齐到 256 字节边界
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// 创建顶点缓冲区
const vertexBuffer = device.createBuffer({
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
// 创建存储缓冲区(用于 compute shader 输出)
const storageBuffer = device.createBuffer({
size: storageData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
// 定义绑定组布局
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
buffer: { type: 'uniform' }
},
{
binding: 1,
visibility: GPUShaderStage.VERTEX,
buffer: { type: 'uniform', hasDynamicOffset: true }
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
texture: { sampleType: 'float' }
},
{
binding: 3,
visibility: GPUShaderStage.FRAGMENT,
sampler: { type: 'filtering' }
}
]
});
// 创建绑定组实例
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: uniformBuffer } },
{ binding: 1, resource: { buffer: dynamicBuffer, offset: 0, size: 64 } },
{ binding: 2, resource: textureView },
{ binding: 3, resource: sampler }
]
});
// 更新动态缓冲区内容(不重新创建缓冲区)
function updateDynamicBuffer(data: Float32Array, offset: number) {
device.queue.writeBuffer(dynamicBuffer, offset, data);
}
关键技术点解读:
hasDynamicOffset: true:动态偏移允许在同一个绑定组中复用同一个缓冲区,用不同偏移量表示不同数据区。就像内存分页一样,减少缓冲区数量。device.queue.writeBuffer():异步写入,不阻塞主线程。写入操作先进入命令队列,等 GPU 空闲时执行。绑定组预创建:绑定组在渲染开始前一次性创建好,渲染循环中只需要切换绑定组索引,不需要重建对象。绑定组切换的时间复杂度从 O(n)(WebGL 逐个绑定)降低到 O(1)(直接切换绑定组句柄)。
5.3 脏矩形渲染与批量提交
Figma 的渲染核心策略是基于**脏矩形(Dirty Rectangle)**的增量渲染——只重绘变化区域,而不是整个画布:
class DirtyRectRenderer {
private dirtyRects: Set<Rect> = new Set();
private webgpuCommandEncoder: GPUCommandEncoder | null = null;
// 标记需要重绘的区域
markDirty(rect: Rect) {
this.dirtyRects.add(rect);
}
// 批量提交渲染命令
flush(device: GPUDevice, renderPass: GPURenderPassEncoder) {
if (this.dirtyRects.size === 0) return;
// 合并重叠的脏矩形,减少绘制调用
const mergedRects = this.mergeDirtyRects([...this.dirtyRects]);
for (const rect of mergedRects) {
// 设置视口到脏矩形区域
renderPass.setViewport(
rect.x, rect.y,
rect.width, rect.height,
0.0, 1.0
);
// 渲染该区域的内容
this.renderRect(device, renderPass, rect);
}
// 清空脏矩形列表
this.dirtyRects.clear();
}
// 合并重叠矩形(贪心算法)
private mergeDirtyRects(rects: Rect[]): Rect[] {
if (rects.length <= 1) return rects;
// 按 x 坐标排序
rects.sort((a, b) => a.x - b.x);
const merged: Rect[] = [];
let current = rects[0];
for (let i = 1; i < rects.length; i++) {
if (this.rectsOverlap(current, rects[i])) {
// 合并两个重叠矩形
current = this.unionRect(current, rects[i]);
} else {
merged.push(current);
current = rects[i];
}
}
merged.push(current);
return merged;
}
}
在 WebGPU 架构下,批量提交的优势更加明显。由于命令编码器是异步的,所有脏矩形的渲染命令可以在一次 GPUQueue.submit() 调用中全部提交,GPU 端只需一次驱动调用即可处理所有绘制命令。
六、性能优化:WebGPU 的计算着色器红利
6.1 计算着色器的应用场景
WebGPU 最令人兴奋的新特性是计算着色器(Compute Shader)。它允许在 GPU 上执行任意通用计算,不限于传统的图形渲染流水线。
在 Figma 的迁移中,计算着色器被应用到了多个关键场景:
场景一:图像模糊渲染优化
传统的模糊效果(高斯模糊、背景模糊)需要多次对纹理进行卷积操作。在 WebGL 下,每次数值处理都需要"纹理→CPU→计算→GPU回传"的过程,数据在显存和主存之间来回搬运。
使用 WebGPU 计算着色器后,所有计算在 GPU 上完成:
// 高斯模糊计算着色器
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
@group(0) @binding(1) var outputTexture: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(2) var<uniform> Params: BlurParams;
struct BlurParams {
kernelSize: u32,
sigma: f32,
direction: u32, // 0=水平, 1=垂直
}
fn gaussianWeight(x: f32, sigma: f32) -> f32 {
let exponent = -(x * x) / (2.0 * sigma * sigma);
return exp(exponent) / (2.506628 * sigma);
}
@compute @workgroup_size(16, 16)
fn main(
@builtin(global_invocation_id) globalID: vec3<u32>,
@builtin(local_invocation_id) localID: vec3<u32>,
) {
let texDims = textureDimensions(inputTexture, 0);
let pos = globalID.xy;
if (pos.x >= texDims.x || pos.y >= texDims.y) {
return;
}
let params = Params;
var result = vec4<f32>(0.0);
var totalWeight = 0.0;
var offset: i32 = -i32(params.kernelSize / 2u);
let halfKernel = i32(params.kernelSize / 2u);
// 沿指定方向进行卷积
for (var i: i32 = 0; i32(params.kernelSize); i32++) {
let sampleOffset = (offset + i32(i)) * (params.direction == 0u ? 1 : texDims.x);
let samplePos = vec2<u32>(
pos.x + (params.direction == 0u ? u32(i32(pos.x) + sampleOffset) : 0u),
pos.y + (params.direction == 1u ? u32(i32(pos.y) + sampleOffset) : 0u)
);
if (samplePos.x < texDims.x && samplePos.y < texDims.y) {
let sample = textureSample(inputTexture, sampler, vec2<f32>(f32(samplePos.x) / f32(texDims.x), f32(samplePos.y) / f32(texDims.y)));
let weight = gaussianWeight(f32(i32(params.kernelSize / 2u) - i32(i)), params.sigma);
result = result + sample * weight;
totalWeight = totalWeight + weight;
}
}
result = result / totalWeight;
textureStore(outputTexture, pos, result);
}
场景二:文本布局计算
Figma 的文本排版涉及复杂的断行算法(Knuth-Plass 段落排版)。对于大量文本,计算量巨大。将断行计算迁移到计算着色器后,文本布局性能提升约 3-5 倍。
6.2 性能实测数据
Figma 团队公布的迁移后性能数据令人振奋:
| 指标 | WebGL 版本 | WebGPU 版本 | 提升幅度 |
|---|---|---|---|
| 复杂文档帧率 | 34 fps | 61 fps | +79% |
| 高斯模糊渲染(4K分辨率) | 2.3 秒 | 0.4 秒 | ~6倍 |
| 纹理切换耗时(50个图层) | 180 ms | 12 ms | 15倍 |
| 内存峰值占用 | 1.8 GB | 0.9 GB | -50% |
| 协作同步延迟 | 85 ms | 28 ms | -67% |
这些数字背后,是架构层面的根本性改变带来的红利,而非局部优化。
七、兼容性挑战与设备适配
7.1 macOS 的 Metal 适配
Figma 用户中 macOS 设备占比超过 40%,而 macOS 上的 WebGPU 实现底层依赖 Apple 的 Metal 框架。这意味着 WebGPU 的行为在不同 macOS 版本上有显著差异:
macOS 13+ (Ventura):Metal 3.0 完整支持,WebGPU 所有特性可用
macOS 12 (Monterey):Metal 2.0,compute shader 支持不完整
macOS 11 (Big Sur):Metal 2.0,存储纹理不支持
Figma 的适配策略:对不同 Metal 版本使用特性检测,而非仅检测操作系统版本。有些用户通过 Homebrew 安装了新版 Metal 驱动,即使操作系统较旧也能支持 WebGPU。
7.2 Windows 的 DirectX 12 适配
Windows 上的 WebGPU 底层基于 DirectX 12(D3D12)。但 D3D12 的 WebGPU 实现存在一个恼人的兼容性问题:纹理格式差异。
某些 D3D12 设备不支持 rgba8unorm 格式,需要使用 rgba8snorm 或 bgra8unorm。Figma 的解决方案是动态选择最优纹理格式:
async function selectOptimalTextureFormat(device: GPUDevice): Promise<GPUTextureFormat> {
const preferredFormat = navigator.gpu.getPreferredCanvasFormat();
// 测试目标格式是否被设备支持
const testTexture = device.createTexture({
size: [1, 1],
format: 'rgba8unorm',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
try {
// 如果设备不支持,会在创建时抛出异常
testTexture.destroy();
return 'rgba8unorm';
} catch {
// 回退到 bgra8unorm
return 'bgra8unorm';
}
}
7.3 Linux 和移动端的特殊处理
Linux 上 WebGPU 支持率最低(约 35% 设备可用),主要原因是 Wayland 和 X11 窗口系统下 WebGPU 实现不一致。Figma 对 Linux 用户的建议是使用 Chrome 的 --enable-unsafe-webgpu 标志,或继续使用 WebGL 版本。
移动端(iOS/Android)情况相对简单:iOS 16+ 的 Safari 内置 WebGPU 实现完整;Android Chrome/Edge 从 Chrome 121 开始支持 WebGPU,但设备碎片化严重,需要逐一适配。
八、协作同步的架构升级
8.1 为什么协作同步是迁移的关键战场
Figma 的实时协作功能是其核心竞争力。用户 A 修改了一个按钮的颜色,毫秒级地传送到用户 B 的屏幕上——这背后是一个精密的 CRDT(无冲突复制数据类型)系统。
迁移到 WebGPU 后,协作同步系统也必须升级。原因:
- 帧率依赖性:协作系统需要知道何时画布稳定(无用户操作)才能压缩传输。如果帧率从 34fps 提升到 61fps,同步策略必须重新校准
- 渲染时序变化:WebGPU 的异步命令提交改变了渲染完成的时间点,协作系统需要调整"已完成渲染"的判定逻辑
- GPU 内存共享:跨标签页/标签页间共享渲染资源的场景增加,需要新的资源管理策略
8.2 多标签页资源同步
Figma 支持在浏览器多标签页中打开同一文件进行协作。迁移到 WebGPU 后,一个标签页中的 WebGPU 设备可以直接共享其显存中的纹理数据给另一个标签页:
// 在标签页 A 中:导出共享句柄
async function exportSharedTexture(texture: GPUTexture): Promise<SharedTextureHandle> {
const sharedHandle = (texture as any).createSharedTextureAccess();
return sharedHandle; // 可通过 postMessage 传递给其他标签页
}
// 在标签页 B 中:导入共享句柄
async function importSharedTexture(
device: GPUDevice,
sharedHandle: SharedTextureHandle
): Promise<GPUTexture> {
return device.importSharedTexture(sharedHandle);
}
这个特性使得跨标签页的渲染资源共享成为可能,大幅降低了协作场景下的内存占用。
九、迁移后的工程收益
9.1 开发者体验的提升
对 Figma 的开发者来说,WebGPU 迁移带来了实实在在的开发体验改善:
类型安全:WGSL 的强类型系统配合 TypeScript 的类型检查,着色器代码的错误可以在编译期发现,而不是在浏览器中崩溃
调试工具:Chrome DevTools 内置了 WebGPU 专用调试器,可以逐帧查看命令缓冲区内容、GPU 内存使用情况、着色器资源绑定状态
标准化:不再需要针对不同浏览器引擎(V8/JavaScriptCore/Chakra)维护不同的 WebGL hack,WebGPU 的跨浏览器行为一致性更高
9.2 未来可期的优化空间
迁移完成只是起点,以下优化方向已在 Figma 团队的路线图上:
GPU 驱动的间接绘制(Indirect Drawing):减少 CPU → GPU 的绘制调用数量
纹理流式加载(Texture Streaming):对于超大型设计文件,按需加载可视区域纹理,而非一次性加载全部
计算着色器的深度优化:将更多 CPU 端计算迁移到 GPU(路径运算、布尔运算、文本布局)
Metal/DirectX/Vulkan 原生后端:绕过浏览器抽象层,直接对接操作系统原生图形 API,进一步压榨性能
十、给前端开发者的迁移启示
Figma 的 WebGPU 迁移经验,对所有前端图形应用都有参考价值:
10.1 何时该迁移
不是所有 WebGL 应用都需要迁移到 WebGPU。如果你的应用满足以下条件,可以考虑迁移:
- 渲染负载重,频繁遇到性能瓶颈
- 需要 compute shader 能力(如复杂图像处理、AI 推理)
- 目标用户设备 WebGPU 支持率高
- 团队有图形学方向的技术储备
如果你的应用只是一个简单的 2D 图表或偶尔的 3D 模型展示,WebGL 仍然够用。迁移成本不容忽视,不要为了"赶时髦"而迁移。
10.2 迁移前的准备工作
- 建立性能基准:用 WebGL 在目标场景下测量真实性能数据,作为迁移后的对比基准
- 着色器目录梳理:统计现有着色器数量、复杂度、关键路径,识别转换工作量
- 用户设备分布分析:分析用户设备的 WebGPU 支持率,确定合理的回退策略
- 自动化测试覆盖:为图形输出正确性建立自动化测试(快照对比),防止迁移引入视觉回归
10.3 分阶段迁移的执行建议
Figma 的经验表明,最稳妥的路径是:
阶段一(1-2月):基础设施搭建
- WebGPU 渲染器独立实现
- 自动着色器转换器开发
- 基准性能测试建立
阶段二(2-3月):功能覆盖
- 核心渲染路径 WebGPU 实现
- 脏矩形、批处理等优化特性
- 自动化视觉回归测试
阶段三(3-4月):灰度发布
- 5% 用户启用 WebGPU
- 监控崩溃率、帧率数据
- 迭代修复问题
阶段四(持续):全量发布与优化
- 逐步扩大 WebGPU 用户比例
- 根据用户反馈优化体验
- 探索高级特性(compute shader 等)
整个迁移过程历时约半年,这还是 Figma 这样一个有深厚图形学积累的团队。对于大多数团队,估算 6-12 个月的迁移周期是合理的。
结语:站在巨人的肩膀上继续攀登
Figma 的 WebGPU 迁移,是 2026 年前端图形领域最值得关注的工程事件之一。它不仅是一次技术升级,更是一种示范——如何在保障用户体验的前提下,完成核心基础设施的代际更替。
当你在浏览器里流畅地缩放一个包含数百个图层的复杂设计文件时,当你看到设计师实时协作时毫无延迟的同步效果时,这些体验背后,是 Figma 团队数百个着色器的精心翻译、是三层回退机制的周密设计、是跨平台适配的耐心打磨。
WebGPU 正在重新定义浏览器图形能力的上限。而 Figma 用实际行动证明:最好的技术升级,是用户感受不到迁移痕迹的升级。
参考来源与深入阅读:
- Figma Engineering Blog: WebGPU Migration(官方工程博客)
- WGSL 规范文档(W3C 官方规范)
- WebGPU API 文档(MDN)
- tool.lu - Figma WebGPU 迁移解析
- WebGPU 性能基准测试
本文首发于程序员茄子(chenxutan.com),保留所有权利。