Why path.transform() Killed Performance
Animate a rotated rectangle. 60fps. Smooth.
Animate a vector network with 50 segments. 15fps. Choppy.
Same animation. Different data structure. 4× performance difference.
The bottleneck: path.transform() happens on the CPU.
The Problem
Every frame, for every animated element:
SkPath originalPath = getPath();
SkMatrix transform = getAnimationMatrix(); // Rotation, scale, etc.
SkPath transformedPath;
originalPath.transform(transform, &transformedPath);
canvas.drawPath(transformedPath, paint);
The transform() call recalculates every vertex of the path on the CPU. For a 50-segment vector network with bezier curves, that's ~200 points recalculated every frame.
At 60fps, that's 12,000 point transformations per second. On the CPU. Single-threaded.
For static geometry, this is fine—transform once, cache the result. But for animations, you're recalculating the same transform 60 times per second.
First Attempt: Cache Transformed Paths
Maybe cache the transformed paths?
std::map, SkPath> transformCache;
SkPath getTransformedPath(PathID id, const SkMatrix& matrix) {
auto key = {id, matrix};
if (!transformCache.contains(key)) {
transformCache[key] = originalPath.transform(matrix);
}
return transformCache[key];
}
This worked for static transforms. For animations, every frame has a different matrix—the cache never hits. Cache miss rate: 100%.
We're still doing CPU path transformation every frame.
The Fundamental Issue
GPUs are really good at matrix transformations. That's what they're designed for—transforming millions of vertices per frame.
But SkPath is a CPU data structure. To render it, Skia:
- Transforms the path on CPU (via
path.transform()) - Tesselates the curved path into triangles (CPU)
- Uploads vertices to GPU
- GPU renders the triangles
Steps 1-2 happen every frame for animated paths. That's the bottleneck.
The Solution: Expose the Mesh Interface
Skia has SkMesh—a way to pass vertices directly to the GPU with a transform applied by vertex shaders.
We exposed it to CanvasKit:
// Build mesh once (CPU cost paid once)
const meshData = {
vertices: new Float32Array([...v]), // All vertex positions
indices: new Uint16Array([...c]), // Triangle connectivity
uvs: new Float32Array([...coord]) // Texture coordinates
};
const mesh = CanvasKit.MakeMesh(meshData, ...m);
// Render with transform (GPU cost, very cheap)
function render(canvas, transform) {
canvas.save();
canvas.concat(transform); // GPU applies this
canvas.drawMesh(mesh, paint);
canvas.restore();
}
Now the workflow is:
- Build mesh once on CPU
- Upload to GPU once
- Every frame: Send a transform matrix (16 floats = 64 bytes)
- GPU transforms vertices in parallel
- GPU renders
The expensive CPU work (path tessellation, vertex generation) happens once. Animation just sends new matrices to the GPU.
The Performance Difference
Before (path.transform):
- CPU: Transform 200 points × 60fps = 12,000 transforms/sec
- CPU: Tessellate curves into triangles every frame
- Frame time: ~16ms on CPU
After (mesh + GPU transform):
- CPU: Send 16 floats to GPU
- GPU: Transform 200 vertices in parallel (microseconds)
- Frame time: ~0.5ms on CPU
30× faster for animated vector graphics.
This is why Figma uses raw WebGL instead of higher-level Canvas APIs or CanvasKit for their main rendering. GPU vertex transforms are fundamentally faster than CPU path operations for animated content.
The Trade-off
Meshes are less flexible than paths:
- Can't use path operations (union, intersection)
- Can't apply path effects (dashes, etc.) after mesh creation
- Stroke geometry must be pre-computed into mesh
But for rendering animated vector graphics, the performance win is worth it.
We use:
- Paths for static content and path operations
- Meshes for animated content that needs fast transforms
Generate the mesh from the path once, then animate the mesh.
Results
Animated vector networks now run at 60fps even with complex geometry. The mesh interface exposed ~200 lines of CanvasKit bindings, but the performance improvement is essential for smooth animation.
The CPU-GPU boundary matters. Path transforms are CPU-bound. Vertex transforms are GPU-parallel. For animation, you want the latter.
Read next: Image Filters: Exposure, Contrast, and the Missing Matrix - Why ColorMatrix isn't enough.