【テクニカル・上級編】Sketchドキュメントの構造監査:不要なアセットや未使用シンボルを自動クリーンアップする最適化術 – UI/UX・デザインツール活用バイブル

Sketchドキュメント構造解析とヘッドレス・ガベージコレクション:DesignOpsにおける自動クリーンアップ・パイプラインの構築

プロダクトが成熟し、デザインシステムが数百のコンポーネントへ肥大化したとき、Sketchファイル(`.sketch`)は時に数百メガバイトの重厚な「データ・モンスター」へと変貌する。動作の遅延、レンダリングのハングアップ、Git LFSの容量圧迫、そしてCI/CDパイプラインにおけるビルド時間の悪化——これらの元凶は、単に「画像が多いから」ではない。内部描画ツリーにおけるデッドコード(未使用シンボル)、孤立したバイナリアセット(Orphaned Bitmaps)、そして重複定義された共有スタイルの蓄積が原因である。

GUI上の既存プラグインをボタンポチポチと押すだけの手動掃除は、スケールする組織においては非生産的であり、脆い。本稿では、`.sketch` ドキュメントを単なる「デザインデータ」ではなく構造化されたバイナリ/JSONアーカイブとして解剖し、Node.jsおよびCLIによるヘッドレス静的解析と自動ガベージコレクション(GC)を実行するパイプラインを構築する。

—

1. 敵を知る:`.sketch` ファイルの内部アーキテクチャと肥大化メカニズム

Sketchドキュメントの正体は、Open Packaging Conventionsに類似したZIP圧縮されたJSON構造体である。`.sketch` ファイルを解凍すると、以下のディレクトリ構造が現れる。

my-design-system.sketch (ZIP Archive)
├── document.json # カラーパレット、共有スタイル(Layer/Text)、ライブラリ参照
├── meta.json # 創作者情報、Sketchバージョン、ページ一覧
├── user.json # キャンバスのズーム状態や表示領域(ローカル状態)
├── pages/
│ ├── 12345678-….json # ページ単位の全レイヤーAST(Abstract Syntax Tree)
│ └── 87654321-….json
└── images/ # 埋め込まれたビットマップ画像(PNG/JPEG/WEBP)
├── a1b2c3d4….png
└── e5f6g7h8….png

なぜファイルは肥大化し、重くなるのか?

1. シンボルツリーの孤立(Orphaned `symbolMaster`)
デザイン変更によりキャンバス上の `symbolInstance` を削除しても、別ページや別ライブラリに存在する `symbolMaster` は自動削除されない。さらに、シンボル内に「ネストされたシンボル」が存在する場合、参照カウントの追跡がGUI上では不可能になる。
2. 非参照ビットマップの残存(Unreferenced Images)
レイヤーの背景画像やオーバーライドで画像を一度適用し、後にレイヤーを削除または別の画像に差し替えても、`images/` ディレクトリ内のバイナリファイルは削除されず、アーカイブ内に永続化される。
3. Foreign Style / Duplicate Palette の増殖
外部ライブラリからコンポーネントをコピペするたび、`document.json` の `foreignStyles` や `foreignSymbols` にスキーマの複製が書き込まれる。同じ `#0052CC`(Brand Blue)であっても、異なるIDを持つRGBAオブジェクトとして無数に重複生成される。

—

2. アルゴリズム設計:参照グラフの巡回とマーク&スウィープ

自動クリーンアップ・エンジンの核となるのは、コンパイラ言語のメモリ管理で用いられるマーク&スウィープ(Mark-and-Sweep)アルゴリズムである。

[ルート要素の抽出]
│
├── Pages (全レイヤーAST)
│ ├── SymbolInstances (`symbolID`) ────────┐ (マーク)
│ └── Image Fills / Layers (`image._ref`)─┼───────┐
│ ▼ ▼
└── Document [Active [Active
├── SharedStyles Symbols] Images]
└── Swatches │ │
│ │ (比較)
▼ ▼
[Master [files in
Symbols] images/]
│ │
└───┬───┘
▼
[Unreferenced Delete]

抽出とトラバースのロジック

1. マークフェーズ(Mark Phase):

  • 全 `pages/.json` を走査し、実行中のキャンバスに配置されている全 `symbolInstance` の `symbolID` を収集。
  • 重要: `symbolInstance` の `overrideValues` 内に含まれる「オーバーライドされたシンボルID」および「オーバーライドされた画像参照(`_ref`)」も再帰的に走査・抽出する。
  • `symbolMaster` 自体の内部レイヤー構造(ネストされたインサイト)を解析し、他の `symbolID` への参照を追跡して依存関係グラフ(DAG: Directed Acyclic Graph)を作成する。
  • レイヤーの `fills`, `borders`, `patternFill` から参照されている全 `image._ref` のハッシュ値を収集する。

2. スウィープフェーズ(Sweep Phase):

  • シンボルスイープ: 到達不能(Unreachable)と判定された `symbolMaster` オブジェクトを AST から完全除去する。
  • アセットスイープ: どのレイヤーからも参照されていない `images/` 内の物理ファイルをディスク/メモリから抹消する。
  • スタイルスイープ: どのレイヤーの `sharedStyleID` とも一致しない `document.json` 内の `sharedStyles` を削除する。

—

3. 実装:完全自動クリーンアップ・スクリプト (Node.js)

以下は、Sketchを起動せずに(ヘッドレスで)`.sketch` ファイルを極限まで圧縮・最適化する完全な自動化スクリプトである。

前提要件

npm install jszip glob

`sketch-gc-engine.js`

const fs = require(‘fs-extra’);
const path = require(‘path’);
const JSZip = require(‘jszip’);

class SketchGarbageCollector {
constructor(filePath) {
this.filePath = filePath;
this.zip = new JSZip();
this.documentJson = null;
this.pagesJsons = {}; // pageId -> json
this.referencedSymbolIds = new Set();
this.referencedImageHashes = new Set();
this.allMasterSymbols = new Map(); // symbolID -> { pageId, layerRef }
}

async load() {
const buffer = await fs.readFile(this.filePath);
this.zipArchive = await this.zip.loadAsync(buffer);

// Parse document.json
const docRaw = await this.zipArchive.file(‘document.json’).async(‘string’);
this.documentJson = JSON.parse(docRaw);

// Parse all pages
const pageFiles = Object.keys(this.zipArchive.files).filter(f => f.startsWith(‘pages/’));
for (const pFile of pageFiles) {
const pageRaw = await this.zipArchive.file(pFile).async(‘string’);
this.pagesJsons[pFile] = JSON.parse(pageRaw);
}
}

/

  • ASTを再帰走査して参照(SymbolID, ImageRef)をマークする

/
traverseLayer(layer) {
if (!layer) return;

// 1. Symbol Master の収集
if (layer._class === ‘symbolMaster’) {
this.allMasterSymbols.set(layer.symbolID, layer);
}

// 2. Symbol Instance の参照マーク
if (layer._class === ‘symbolInstance’) {
this.referencedSymbolIds.add(layer.symbolID);

// オーバーライド内のシンボル参照&画像参照の解析
if (layer.overrideValues) {
for (const override of layer.overrideValues) {
if (typeof override.value === ‘string’ && override.value.match(/^[0-9A-F]{8}-[0-9A-F]{4}-/i)) {
// UUID形式のオーバーライド(シンボルID)
this.referencedSymbolIds.add(override.value);
}
if (override.value && override.value._class === ‘MSJSONFileReference’) {
this.referencedImageHashes.add(override.value._ref);
}
}
}
}

// 3. 画像参照(Fills, Borders, Image Layers)のマーク
if (layer.style && layer.style.fills) {
for (const fill of layer.style.fills) {
if (fill.image && fill.image._ref) {
this.referencedImageHashes.add(fill.image._ref);
}
}
}
if (layer._class === ‘bitmap’ && layer.image && layer.image._ref) {
this.referencedImageHashes.add(layer.image._ref);
}

// 子レイヤーの再帰走査
if (layer.layers && Array.isArray(layer.layers)) {
for (const child of layer.layers) {
this.traverseLayer(child);
}
}
}

/

  • 依存関係グラフを解決し、親シンボル経由でのみ参照されている子シンボルをマーク

/
resolveSymbolDependencies() {
let previousSize = 0;
// 依存関係が解決しきるまでループ(推移的閉包の計算)
while (this.referencedSymbolIds.size !== previousSize) {
previousSize = this.referencedSymbolIds.size;
for (const symbolId of Array.from(this.referencedSymbolIds)) {
const master = this.allMasterSymbols.get(symbolId);
if (master) {
this.traverseLayer(master); // Master内部のInstanceやImageを再マーク
}
}
}
}

/

  • スウィープ処理:未使用アセットの除去

/
sweep() {
let removedSymbolsCount = 0;
let removedImagesCount = 0;

// A. 未使用 Symbol Master の削除
for (const [symbolId, masterLayer] of this.allMasterSymbols.entries()) {
if (!this.referencedSymbolIds.has(symbolId)) {
// 全ページからこの symbolMaster を検索して破棄
for (const pagePath of Object.keys(this.pagesJsons)) {
const page = this.pagesJsons[pagePath];
const initialLength = page.layers.length;
page.layers = page.layers.filter(l => l.symbolID !== symbolId);
if (page.layers.length < initialLength) { removedSymbolsCount++; } } } } // B. 未使用 Image バイナリの削除 const imageFiles = Object.keys(this.zipArchive.files).filter(f => f.startsWith(‘images/’));
for (const imgPath of imageFiles) {
// “images/abc…png” -> “images/abc…png”
const refKey = imgPath;
// refs に含まれているかチェック (JSON内での指定フォーマットに正規化)
const isReferenced = Array.from(this.referencedImageHashes).some(ref => imgPath.endsWith(path.basename(ref)));

if (!isReferenced) {
this.zipArchive.remove(imgPath);
removedImagesCount++;
}
}

console.log(`[GC Result] Purged ${removedSymbolsCount} unused Symbol Masters.`);
console.log(`[GC Result] Purged ${removedImagesCount} unreferenced image binaries.`);
}

async save(outputPath) {
// 変更された Pages JSON を ZIP に書き戻す
for (const [pagePath, jsonObj] of Object.entries(this.pagesJsons)) {
this.zipArchive.file(pagePath, JSON.stringify(jsonObj));
}

// ZIP 圧縮オプションの最適化(最高圧縮率を設定)
const outputBuffer = await this.zipArchive.generateAsync({
type: ‘nodebuffer’,
compression: ‘DEFLATE’,
compressionOptions: { level: 9 }
});

await fs.writeFile(outputPath, outputBuffer);
console.log(`[Optimized] Output written to: ${outputPath}`);
}

async execute(outputPath) {
await this.load();

// マークフェーズ
for (const pageJson of Object.values(this.pagesJsons)) {
this.traverseLayer(pageJson);
}

// 依存解決
this.resolveSymbolDependencies();

// スウィープフェーズ
this.sweep();

// 出力
await this.save(outputPath);
}
}

// 実行例
(async () => {
const gc = new SketchGarbageCollector(‘./design_system_legacy.sketch’);
await gc.execute(‘./design_system_optimized.sketch’);
})();

—

4. カラーパレット & スタイル重複の数学的正規化

JSON内部のゴミを減らすだけでなく、ベクトルデータとしての「色定義の正規化(Deduplication)」を行うことで、レンダリングエンジンの描画パス計算を最適化できる。

Sketchでは、同じ `#0052CC` であっても浮動小数点の精度差異(例: `red: 0.0` vs `red: 0.000000001`)により、異なるカラーオブジェクトとして保持されることがある。これをハッシュマップを用いて同一視し、リダイレクトする手法をとる。

/

  • カラーオブジェクトの決定論的ハッシュキー生成

/
function createColorHash(colorObj) {
const r = Math.round(colorObj.red 255);
const g = Math.round(colorObj.green 255);
const b = Math.round(colorObj.blue 255);
const a = Math.round(colorObj.alpha 100) / 100;
return `rgba(${r},${g},${b},${a})`;
}

/

  • document.json 内の Swatches (カラーパレット) 重複統合

/
function deduplicateSwatches(documentJson) {
const colorMap = new Map(); // colorHash -> swatchID
const swatches = documentJson.sharedSwatches ? documentJson.sharedSwatches.objects : [];

const uniqueSwatches = [];

for (const swatch of swatches) {
const hash = createColorHash(swatch.value);
if (!colorMap.has(hash)) {
colorMap.set(hash, swatch.do_objectID);
uniqueSwatches.push(swatch);
} else {
console.log(`[Deduplicate] Merging redundant swatch ID: ${swatch.do_objectID} -> ${colorMap.get(hash)}`);
// ここでレイヤー側の Swatch ID 参照を置き換えるリダイレクトマップを作成・適用する
}
}

documentJson.sharedSwatches.objects = uniqueSwatches;
}

—

5. DesignOps パイプラインへの組み込み (CI/CD)

このガベージコレクションを個人のローカル環境で終わらせず、GitHub ActionsやGitLab CI/CDのパイプラインに組み込むことで、デザインシステムの「非劣化(Entropy Suppression)」を自動保証する。

以下は、デザイナーが Sketch ファイルを Push または Pull Request 作成した際に、自動で構造診断および最適化を実行し、ファイルサイズの肥大化を検知して PR にコメントする GitHub Actions ワークフローの例である。

name: Sketch File Structural Audit & Optimization

on:
pull_request:
paths:

  • ‘design-tokens//.sketch’

jobs:
audit-and-optimize:
runs-on: ubuntu-latest
steps:

  • name: Checkout Code

uses: actions/checkout@v3

  • name: Setup Node.js

uses: actions/setup-node@v3
with:
node-version: 18

  • name: Install Dependencies

run: npm install jszip fs-extra glob

  • name: Run Sketch Structural GC Engine

run: |
node ./scripts/sketch-gc-engine.js \
–input ./design-tokens/core-system.sketch \
–output ./design-tokens/core-system.sketch

  • name: Check File Size Metrics

id: metrics
run: |
ORIGINAL_SIZE=$(git show HEAD:design-tokens/core-system.sketch | wc -c)
NEW_SIZE=$(wc -c < ./design-tokens/core-system.sketch) DIFF=$((ORIGINAL_SIZE - NEW_SIZE)) echo "size_diff=${DIFF}" >> $GITHUB_OUTPUT
echo “Original: ${ORIGINAL_SIZE} bytes, Optimized: ${NEW_SIZE} bytes”

  • name: Commit Optimized File back to PR

if: steps.metrics.outputs.size_diff > 0
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: “chore(designops): automated sketch cleanup and binary compression [skip ci]”
file_pattern: ‘design-tokens//.sketch’

—

6. まとめ:データ構造の美しいデザインこそが、UI/UXの真の基盤である

UI/UXの改善と聞くと、視覚的なマイクロインタラクションやアクセシビリティの向上に目を奪われがちだ。しかし、システムを支えるデザインデータの内部構造が肥大化・腐敗(Software Rot)していては、開発者へのハンドオフは滞り、ビルドパイプラインは鈍化し、プロダクトの進化スピードは確実に削がれる。

1. Sketchファイルは「ZIP + JSON」の純粋な状態空間であると理解すること。
2. GUI操作に依存せず、ASTのトラバースとマーク&スウィープによる自動クリーンアップを導入すること。
3. カラー定義・スタイルのハッシュ化とDesignOps(CI/CD)による品質の継続的統合を行うこと。

コードと同様に、デザインデータもまた洗練された「アーキテクチャ」を持たなければならない。極限までクリーンに削ぎ落とされたSketchファイルは、UIコンポーネントの高速なレンダリングを実現し、デザイナーとエンジニアの連携速度を異次元へと引き上げる。

タイトルとURLをコピーしました