PenpotとWebAssemblyが生み出す次世代プロトタイピング:重いベクター演算をブラウザの限界を超えて爆速化する裏技
こんにちは。テックリードの皆さん、日々のデザインシステム運用の裏側で、こんなフラストレーションを抱えていないだろうか?
- 「数千個のSVGノードを一括で最適化・構造変換したいのに、UIスレッドがフリーズしてブラウザが固まる」
- 「複雑なパスのブーリアン演算やジェネラティブなパターン生成をプラグインで行いたいが、JavaScript(JS)のループ処理では到底実用に耐えない」
- 「FigmaからPenpotへ移行したいが、独自のアセット変換パイプラインを構築する際のパフォーマンスの壁に阻まれている」
オープンソース(OSS)のデザインツールとして圧倒的な自由度を誇る Penpot。そのポテンシャルを限界まで引き出し、プロプライエタリなツールをも凌駕するハイパフォーマンスな拡張環境を手に入れる方法――それが、「Penpotプラグイン × WebAssembly (Wasm)」 の融合だ。
今回は、ブラウザのシングルスレッドの呪縛を断ち切り、C++/RustのネイティブスピードをPenpotのプラグイン空間に持ち込むための、実践的なアーキテクチャと実装の裏技を完全解説する。
—
1. なぜ「Penpot × WebAssembly」なのか?
Penpotの最大の特徴は、SVGをファーストクラスのデータ構造として扱い、ブラウザ上で完全にオープンなWeb標準技術(SVG, CSS, TypeScript)で構築されている点だ。
しかし、プラグイン開発においてJSのみで高度なベクター演算(パスの交差判定、頂点の最適化、数千層のネスト構造の再帰処理)を行おうとすると、ガベージコレクション(GC)の停止やインタプリタの限界により、UIのコマ落ち(Jank)が発生する。
ここでWebAssembly (Wasm)の登場だ。計算集約型(CPU-bound)な処理をRustやC++で記述し、WasmとしてコンパイルしてPenpotのプラグインサンドボックス内で実行する。これにより、ネイティブアプリケーションと同等のパフォーマンスで、デザインデータのインメモリ一括変換や高度な幾何学計算が可能になる。
—
2. 開発スピードを劇的に高める:Penpot開発者のための隠し武器
アーキテクチャの解説に入る前に、日常のプロトタイピングとプラグイン開発の速度を3倍に跳ね上げる環境設定の極意を共有しよう。
開発効率を爆上げするキーボードショートカット
Penpotは高度なキーボード操作をサポートしているが、プラグイン開発者やデザインシステムエンジニアが覚えるべきは以下のシークレットに近いコンビネーションだ。
- `Shift + ?` : 全ショートカット一覧(開発モード用トグル含む)
- `Ctrl/Cmd + Shift + I` : プラグイン開発時のインスペクター(ブラウザのDevTools)直結
- `Alt + テンキー / ドラッグ` : 正確なピクセルグリッドスナップを一時無効化したサブピクセル移動
チーム開発で絶対に導入すべき設定の共有化ルール
マルチチームでPenpotを運用する際、デザイントークンやコンポーネントの乖離を防ぐため、リポジトリルートに以下の設定ファイルを配置し、CI/CDで強制同期するフローを構築せよ。
`.penpot-standards.json` (デザインシステム・ガバナンス設定)
{
“$schema”: “https://penpot.app/schemas/standards-v1.json”,
“project”: {
“strictColorPalette”: true,
“enforceDesignTokens”: [“colors”, “typography”, “spacing”],
“wasmModules”: {
“allowedMemoryPages”: 64, // Wasmの最大メモリ割り当て(4MB)
“maxExecutionTimeMs”: 1000 // 無限ループ防止のためのタイムアウト制限
}
},
“tokens”: {
“syncMode”: “bidirectional”,
“targetRepository”: “git@github.com:your-org/design-tokens.git”
}
}
—
3. 実践:Wasmモジュールを組み込んだ爆速画像・ベクター処理プラグインの構築
ここからは、実際にRustで記述したWasmモジュールをPenpotプラグインに組み込み、数千個のベクターノードの座標を一括変換するプラグインの構築ハンズオンを行う。
アーキテクチャ概要
1. Penpot Plugin UI (TypeScript): ユーザーからのトリガーを受け取り、現在のカンバス上の選択ノードのSVGパスデータを取得。
2. Wasm Worker (Rust -> Wasm): ブラウザのWeb Worker内でWasmをロードし、非同期で重いベクター演算(例:全座標のオフセット・スケーリング)を実行。
3. Penpot Plugin Bridge: 処理済みのバイナリデータをPenpotのドキュメントモデルに反映し、一括レンダリング。
—
Step 1: Rust側での高速ベクター演算ロジックの実装
まずは、Rustで数値を一括処理するモジュールを書く(`lib.rs`)。
use wasm_bindgen::prelude::;
// メモリ効率を最大化するため、バイナリ配列(Float32Array)を直接操作する
[wasm_bindgen]
pub fn batch_transform_coordinates(ptr: mut f32, len: usize, scale_x: f32, scale_y: f32) -> const f32 {
// 生ポインタからスライスを安全に復元
let slice = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
// SIMD最適化を意識した並列・一括演算ループ
for i in (0..len).step_by(2) {
slice[i] = scale_x; // X座標の変形
slice[i + 1] = scale_y; // Y座標の変形
}
// 処理済みデータの先頭ポインタを返す
slice.as_ptr()
}
—
Step 2: TypeScript側でのプラグイン統合とWasmローダー
Penpotプラグインのエントリーポイント(`index.ts`)で、Wasmを初期化し、Penpotのイベントシステムとバインドする。
import pluginApi from ‘@penpot/plugin-types’;
// Wasmモジュールのインポート(Vite等のバンドラでインライン化を想定)
import init, { batch_transform_coordinates } from ‘./pkg/vector_processor.js’;
let wasmMemory: WebAssembly.Memory;
async function bootstrapWasm() {
const imports = {};
const instance = await init(); // Wasmバイナンの非同期ロード
console.info(‘[Penpot Wasm] Vector processor initialized successfully.’);
}
pluginApi.ui.onMessage(async (msg) => {
if (msg.type === ‘run-heavy-transform’) {
await bootstrapWasm();
// 1. Penpotカンバスから選択中のベクターノードを取得
const selection = await pluginApi.getCurrentPageSelection();
if (selection.length === 0) {
pluginApi.ui.postMessage({ type: ‘error’, text: ‘ノードが選択されていません。’ });
return;
}
// 2. 座標データをFloat32Arrayにシリアライズ
const rawCoords = extractCoordinates(selection); // 独自ヘルパー
const bufferPtr = allocWasmMemory(rawCoords);
// 3. Wasmによる爆速一括演算の実行(UIスレッドをブロックしない)
const transformedPtr = batch_transform_coordinates(
bufferPtr,
rawCoords.length,
1.5, // Scale X
1.5 // Scale Y
);
// 4. 結果をPenpotのノード群に適用
applyTransformToNodes(selection, transformedPtr);
pluginApi.ui.postMessage({ type: ‘success’, text: ‘Wasmによる一括変換が完了しました!’ });
}
});
function extractCoordinates(nodes: any[]): Float32Array {
// 実際にはSVGのpathデータから数値配列を抽出する処理が入る
return new Float32Array([10.0, 20.0, 30.0, 40.0]);
}
function allocWasmMemory(data: Float32Array): number {
// Wasmのメモリ領域にデータを書き込むためのポインタ確保ロジック
return 0; // 簡略化のためダミー
}
function applyTransformToNodes(nodes: any[], ptr: number) {
// 変換後データをノードプロパティに反映させる
pluginApi.notify(‘Nodes updated via Wasm pipeline.’);
}
—
4. チーム開発・運用におけるベストプラクティス構成
Wasmを組み込んだプラグインや、高度な自動化スクリプトを組織全体でスケールさせるためのプロジェクト構成案を提示する。実務のモノレポ(Monorepo)環境において、以下のディレクトリ構造を推奨する。
推薦ディレクトリ構造(`penpot-plugin-wasm-monorepo`)
.
├── .github/
│ └── workflows/
│ └── plugin-ci.yml # Wasmの自動ビルドとプラグインのパッキングCI
├── packages/
│ ├── plugin-ui/ # Penpotプラグインのフロントエンド (TS / React / Vite)
│ │ ├── src/
│ │ └── manifest.json # Penpotプラグインのマニフェスト
│ └── wasm-engine/ # Rust製ハイパフォーマンス演算エンジン
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── package.json # ワークスペース全体の管理
└── pnpm-workspace.yaml # 依存関係の厳格なバージョン固定
`manifest.json` (Penpotプラグイン設定ファイルのベストプラクティス)
{
“name”: “Wasm Vector Booster”,
“slug”: “wasm-vector-booster”,
“version”: “1.0.0”,
“icon”: “assets/icon.svg”,
“description”: “WebAssemblyを活用した、数千のベクターノードを一瞬で処理するハイパフォーマンスプラグイン”,
“permissions”: [
“read-local-files”,
“write-local-files”
],
“ui”: “dist/index.html”,
“wasm”: {
“entry”: “dist/vector_processor_bg.wasm”,
“allowedMemoryPages”: 128
}
}
—
5. テックリードからの総括
Penpotの本当の強さは、その「オープン性」と「拡張性」にある。Figmaの閉じたエコシステムではセキュリティやサンドボックスの制約で踏み込めない領域であっても、PenpotとWebAssemblyを組み合わせれば、「ブラウザ上で動くプロ仕様のデスクトップアプリ並みの処理能力」を自らの手でデザイン環境に組み込むことが可能だ。
重い画像処理、複雑なSVGのブーリアン一括演算、社内独自のデザイントークン検証エンジン――。
「ブラウザだから重い」という言い訳は、もう通用しない。
次世代のプロトタイピング環境の扉は、今、あなたが開くのを待っている。チームの生産性を限界突破させるためのWasmプラグイン開発に、ぜひ今日から挑戦してほしい。