Skip to content

Open Pencil vs Penpot: Architecture & Performance Comparison

Why compare? OpenPencil exists because closed design platforms control what's possible. Understanding architectural differences shows what an open, local-first alternative can do differently.

Penpot's WASM renderer

Penpot 2.x includes a Rust/Skia WASM renderer (render-wasm/v1) that can be enabled via server flags or the ?wasm=true URL parameter. The old SVG renderer remains the default. This page covers both.

1. Scale & Codebase Size

MetricOpen PencilPenpot
Total LOC~26,000~299,000
Source files~143~2,900
LanguagesTypeScript, VueClojure, ClojureScript, Rust, JS, SQL, SCSS
Rendering engine~3,200 LOC (TS, 10 files)22,000 LOC (Rust/Skia WASM)
UI code~4,500 LOC~175,000 LOC (CLJS + SCSS)
BackendNone (local-first)32,600 LOC + 151 SQL files
LOC ratio~11×

Open Pencil is ~11× smaller — and that's the whole point. It's not a simplification; it's a fundamentally different architecture.

2. Architecture

Open Pencil: Monolithic Client

┌─────────────────────────────────┐
│         Tauri (native shell)    │
│  ┌───────────────────────────┐  │
│  │  Vue 3 + TypeScript       │  │
│  │  ┌─────────┐ ┌──────────┐│  │
│  │  │  Editor  │ │  Kiwi    ││  │
│  │  │  Store   │ │  Codec   ││  │
│  │  └────┬─────┘ └──────────┘│  │
│  │       │                    │  │
│  │  ┌────▼────────────────┐  │  │
│  │  │  Scene Graph (TS)    │  │  │
│  │  │  Map<string, Node>   │  │  │
│  │  └────┬────────────────┘  │  │
│  │       │                    │  │
│  │  ┌────▼────┐ ┌──────────┐│  │
│  │  │  Skia   │ │  Yoga    ││  │
│  │  │CanvasKit│ │  Layout  ││  │
│  │  │  (WASM) │ │  (WASM)  ││  │
│  │  └─────────┘ └──────────┘│  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘

Everything in one process. No server, no database, no Docker. The scene graph is a flat Map<string, SceneNode> in TypeScript. Rendering calls Skia CanvasKit directly from TS. Layout is Yoga WASM called synchronously.

Penpot: Distributed Client-Server

┌───────────────────────────────────────────────────────┐
│                    Docker Compose                      │
│  ┌──────────────┐  ┌─────────────┐  ┌──────────────┐ │
│  │   Frontend    │  │   Backend   │  │   Exporter   │ │
│  │  ClojureScript│  │   Clojure   │  │  (Chromium)  │ │
│  │  shadow-cljs  │  │   JVM       │  │              │ │
│  │  ┌─────────┐ │  │  ┌────────┐ │  └──────────────┘ │
│  │  │render-  │ │  │  │Postgres│ │                    │
│  │  │wasm     │ │  │  │Valkey  │ │  ┌──────────────┐ │
│  │  │(Rust→   │ │  │  │ MinIO  │ │  │   MCP        │ │
│  │  │ Skia    │ │  │  └────────┘ │  │   Server     │ │
│  │  │ WASM)   │ │  │             │  └──────────────┘ │
│  │  └─────────┘ │  │             │                    │
│  └──────────────┘  └─────────────┘                    │
└───────────────────────────────────────────────────────┘

5+ services minimum. PostgreSQL for persistence, Redis (Valkey) for pub/sub and caching, MinIO for asset storage, a JVM backend, a Node.js exporter (headless Chromium for server-side rendering), plus the ClojureScript frontend. Dev setup requires Docker Compose with custom networking.

Verdict: Architecture

Open Pencil's single-process architecture eliminates:

  • Network latency between frontend and backend
  • Serialization/deserialization overhead at service boundaries
  • Container orchestration complexity
  • Database query overhead for every operation

Penpot's architecture is optimized for multi-user server-hosted deployments. Open Pencil is optimized for instant local performance.

3. Rendering Pipeline

Open Pencil: TS → CanvasKit WASM (direct)

typescript
// renderer.ts — direct CanvasKit calls from TypeScript
renderSceneToCanvas(canvas, graph, pageId) {
  // Iterate nodes, build Skia paths/paints, draw
  this.fillPaint.setColor(...)
  canvas.drawRRect(rrect, this.fillPaint)
}
  • 1 boundary crossing: TS → WASM (CanvasKit)
  • Scene graph lives in JS heap — no serialization to render
  • ~3,200 LOC renderer (split into 10 focused files: scene, overlays, fills, strokes, shapes, effects, rulers, labels)

Penpot: JS (compiled from CLJS) → Rust WASM → Skia

Penpot 2.x includes a Rust/Skia WASM renderer (render-wasm/v1), opt-in via server flags or ?wasm=true. When enabled, shapes are rendered through:

ClojureScript (compiled to JS)
  → decompose to primitives + binary-pack into WASM linear memory
  → Rust WASM (via Emscripten C FFI)
  → skia-safe (Rust Skia bindings)
  → Skia (WebGL)

When disabled (default), shapes render as an SVG DOM tree via React/Reagent — each shape is a DOM element.

  • 1 boundary crossing (JS → WASM), same as Open Pencil — but with explicit serialization overhead: UUIDs split to 4×u32, transforms to 6×f32, fills/strokes binary-packed, base props batched into a 104-byte struct per shape
  • Tile-based rendering system with interest areas
  • 11 separate render surfaces (fills, strokes, shadows, etc.)
  • Global mutable state via unsafe { STATE.as_mut() } pattern
  • 22,000 LOC Rust render engine

Penpot's tile system (TileViewbox, TileTextureCache, TILE_SIZE_MULTIPLIER) pre-renders tiles around the viewport and caches textures (up to 1024 entries).

Open Pencil re-renders the full viewport every frame because CanvasKit called directly from TS is fast enough to not need tiling.

Verdict: Rendering

AspectOpen PencilPenpot
JS→WASM boundaryDirect (TS objects)Binary-packed (104-byte base props struct)
Rendering modelImmediate/full redrawTile-cached
Surface management1 surface11 surfaces
Memory overheadLow (no tile cache)High (1024 tile cache)
Code complexity~3,200 LOC (10 files)22,000 LOC
Unsafe codeNoneunsafe global state

When Penpot's WASM renderer is enabled, both projects use Skia via JS→WASM. Open Pencil calls CanvasKit directly with TS objects. Penpot decomposes ClojureScript data into binary-packed structs, writes them to WASM linear memory, and renders through a 22,000 LOC Rust engine. When WASM is disabled (default), Penpot renders shapes as an SVG DOM tree. For small-to-medium documents, the direct CanvasKit path is faster. Penpot's tile system may win on extremely large canvases (100K+ shapes) where only a small viewport is visible — but the overhead is significant.

4. Scene Graph & Data Model

Open Pencil

typescript
// Flat map, O(1) lookup
nodes: Map<string, SceneNode>
// 29 node types from Figma's Kiwi schema
// ~390 fields per NodeChange (Figma-compatible)
  • TypeScript interfaces with strict types
  • GUIDs match Figma's sessionID:localID format
  • Direct property access — no indirection layers

Penpot

clojure
;; 20+ type definition files in common/src/app/common/types/
;; shapes_builder.cljc, shapes_helpers.cljc
;; Separate type systems for: color, component, container, fills,
;; grid, modifiers, objects_map, page, path, etc.
  • Data spread across common/ (49,600 LOC of .cljc)
  • Separate geometry modules for flex layout (~6 files), grid layout (~5 files), constraints, bounds, corners, effects
  • Runtime schema validation (Malli)
  • Data must cross CLJS→Rust boundary for rendering

Verdict: Data Model

Open Pencil reuses Figma's proven schema (194 Kiwi definitions) directly in TypeScript — zero translation. Penpot maintains its own type system across Clojure/ClojureScript/Rust, requiring manual sync between all three.

5. Layout Engine

Open Pencil: Yoga WASM (314 LOC)

typescript
import Yoga from 'yoga-layout'
// Direct mapping: Figma stack* fields → Yoga flex properties
const root = Yoga.Node.create()
root.setFlexDirection(FlexDirection.Row)
root.calculateLayout()
applyYogaLayout(graph, frame, yogaRoot)

314 lines total. Synchronous, in-process.

Penpot: Dual Implementation

  1. ClojureScript (common): flex_layout/ (6 files), grid_layout/ (5+ files) — custom implementations
  2. Rust WASM: flex_layout.rs (741 LOC), grid_layout.rs (843 LOC) — reimplemented from scratch

Penpot maintains two independent layout engines (CLJS and Rust) that must produce identical results.

Verdict: Layout

Open Pencil delegates to a battle-tested library (Yoga, used by React Native on billions of devices) in 314 lines. Penpot maintains ~3,000+ LOC of custom layout code duplicated across two languages.

6. File Format & Figma Compatibility

Open Pencil

  • Native Kiwi binary format — same serialization as Figma uses internally
  • Direct .fig file import via extracted Kiwi codec (2,178 LOC schema + 551 LOC codec)
  • Figma clipboard paste support (reads Figma's Kiwi binary from the clipboard)
  • Wire-compatible with Figma's multiplayer protocol

Penpot

  • ZIP archive (.penpot files) containing JSON manifests, per-file JSON data, binary assets, and thumbnails (v3 format)
  • SVG used for default rendering and export (opt-in WASM renderer available)
  • No native .fig import
  • Three format versions (v1 legacy Transit, v2, v3 JSON-in-ZIP) with migration system

Verdict: File Format

Open Pencil has a significant advantage — it can read Figma files natively and even paste Figma clipboard data. Penpot requires manual export/import and cannot open .fig files.

7. State Management & Undo

Open Pencil

typescript
// 110 LOC — inverse command pattern
class UndoManager {
  apply(entry: UndoEntry) { entry.forward(); this.undoStack.push(entry) }
  undo() { entry.inverse(); this.redoStack.push(entry) }
}

110 lines. Forward/inverse closures that capture minimal state. Batch support for multi-step operations.

Penpot

State management uses Potok (a Redux-like library for ClojureScript atoms). Events implement UpdateEvent (pure state→state) or WatchEvent (side effects via RxJS). Undo stores inverse change vectors (max 50 entries), with transactions to group rapid changes and auto-expiry after 20 seconds.

Verdict: State

Open Pencil's approach is simpler and lower overhead. Penpot's approach is more suitable for collaboration (changes are serializable), but at the cost of complexity.

8. Developer Experience

MetricOpen PencilPenpot
Dev setupbun install && bun devDocker Compose + JVM + Node + Rust toolchain
Hot reloadVite HMR (~50ms)shadow-cljs (seconds)
Type checkingTypeScript (strict)Runtime (Malli schemas)
Build time<5s (Vite)Minutes (JVM startup + CLJS compile + Rust WASM)
First contribution barrierLow (TS/Vue)High (Clojure + Rust + Docker)
DesktopTauri v2 (~5MB)N/A (browser-only)
Hiring poolMassive (TS/Vue devs)Tiny (ClojureScript + Rust)

9. Performance Characteristics

ScenarioOpen PencilPenpot
Cold start<2s (WASM load)10s+ (server + client + WASM)
Operation latency<1ms (in-process)10-50ms (network round-trip)
Render frameDirect Skia callCLJS→JS→WASM FFI→Skia
Memory baseline~50MB (browser tab)~300MB+ (JVM + Postgres + Valkey + browser)
Offline capabilityFull (local-first)None (server-dependent)
10K shapes renderOne pass, no cachingTile-based with 11 surfaces

10. What Penpot Does Better

  1. Server-side collaboration — centralized multi-user editing with WebSockets, user accounts, and access control (Open Pencil uses P2P via Trystero + Yjs — no server, but also no access control or persistence beyond the session)
  2. PDF export — headless Chromium export service for PDF rendering (OpenPencil exports SVG but not PDF yet)
  3. Plugin system — full plugin API with sandboxed execution
  4. Design tokens — native design token support
  5. CSS Grid layout — custom implementation (Open Pencil uses Yoga fork with grid support)
  6. Self-hosting — Docker-based deployment for teams
  7. Maturity — years of production usage, battle-tested at scale

11. Scripting & Extensibility

OpenPencil ships with an eval command that provides a Figma-compatible Plugin API for headless scripting — batch operations, automated testing, and AI-driven modifications all run without the GUI. On top of that, 90 AI tools are available via built-in chat, MCP server (stdio + HTTP), and the CLI — covering read, create, modify, structure, variables, vector path, analyze (color/typography/spacing/clusters), diff, boolean operations, and arrangement. Penpot has a plugin system with sandboxed execution but no headless scripting API or MCP integration.

Summary

DimensionWinnerWhy
Architecture simplicityOpen PencilSingle process vs 5+ services
Rendering performanceOpen PencilDirect CanvasKit vs SVG DOM (default) or binary-packed WASM
Code maintainabilityOpen Pencil~26K LOC in 1 language vs 299K in 4+ languages
Figma compatibilityOpen PencilNative Kiwi codec vs no .fig support
Developer onboardingOpen PencilTS/Vue vs Clojure/Rust/Docker
Desktop experienceOpen PencilTauri native vs browser-only
Layout engineOpen PencilYoga (proven) vs custom dual implementation
CollaborationTiePenpot: server-based with access control; Open Pencil: P2P via Trystero + Yjs, zero hosting
Self-hostingPenpotDocker-ready vs desktop-only
Ecosystem maturityPenpotYears of production vs early stage

Open Pencil is architecturally leaner — a single-process CanvasKit renderer in ~26K LOC of TypeScript, Figma-compatible by design. Penpot is a full-stack platform with ~299K LOC across Clojure, ClojureScript, Rust, and SCSS, plus a Docker service fleet. Both now offer real-time collaboration (different architectures: P2P vs server). Penpot has a plugin ecosystem and server-side PDF export; Open Pencil has Figma-compatible headless scripting, 90 AI/MCP tools, SVG export, and a native desktop app.

Released under the MIT License.