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 ratio1×~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.