AI活用

ComfyUIカスタムノードにJavaScript UIを追加する|Widgetとイベント処理

ComfyUIカスタムノードにJavaScriptでボタンや表示を足す方法を解説。WEB_DIRECTORYの公開、app.registerExtensionのhook、Widget値がworkflow JSONに残る仕組み、Nodes 2.0での互換確認、UI例外からBackend処理を守る設計まで扱います。

この記事の目次
  1. 結論:UIは公式hookに小さく載せ、データはPython側だけが持つ
  2. BackendとFrontendを分離する
  3. 最小UI拡張を作る
  4. Widget値を同期する
  5. イベントを増やしすぎない
  6. Nodes 2.0との互換性を確認する
  7. 同じノードを両UIで保存→再読込まで検証する
  8. UI障害から処理を守る
  9. よくある質問
  10. ノードの描画を丸ごと自分で描き替えたいのですが。
  11. JS側で計算した値をPython側に渡せますか?
  12. hookの一覧はどこで確認できますか?
  13. まとめ

ComfyUIのカスタムノードにJavaScriptのUIを足すには、Python側でWEB_DIRECTORYを公開し、そのfolderのJSからapp.registerExtensionで公式のhookに処理を登録します。最初はボタン1つか表示1つの小さな拡張で始め、Widgetの値がworkflow JSONにどう残るかを保存→再読込で確かめてから広げます。

情報確認日:2026年8月22日(日本時間)

結論:UIは公式hookに小さく載せ、データはPython側だけが持つ

責任範囲

  • Pythonの生成処理とJSのUI拡張を別責務に分ける境界
  • WEB_DIRECTORYregisterExtensionによる最小UI拡張
  • Widgetの値が保存workflowにどう残るかの確認手順
  • イベント購読を増やしすぎない範囲の決め方
  • Nodes 2.0と旧UIの両方で表示・保存・再読込を検証する記録表
  • Frontendの例外がBackendのデータを壊さない設計

Python側の最小ノードの作り方と登録はComfyUIカスタムノードの作り方に、Nodes 2.0とは何か・切り替え方・互換性の全体像はComfyUI Nodes 2.0とはにあります。この記事はその上に載せるFrontend側だけを扱い、ReactやVueの一般解説、テストと公開の自動化は扱いません。

スポンサーリンク

BackendとFrontendを分離する

カスタムノードの「正しい値」を持つのはPython側です。JS側は、その値を見やすく表示したり、入力を楽にしたりする層で、JSがなくてもノードが動く状態を保ちます。この境界を崩すと、UIの不具合が生成結果の不具合になります。

責務 Python(Backend) JavaScript(Frontend)
入力の定義 INPUT_TYPESで型・既定値・範囲を決める 決めない。表示と補助だけ
値の検証 実行時に必ず行う 行ってもよいが、通らなくてもBackendが守る前提
生成処理 すべて なし
保存される値 INPUT_TYPES由来のwidget値 独自に足した値は保存先を自分で設計する
失敗したとき エラーを返し実行を止める 表示が崩れるだけで、実行は止めない

公式ドキュメントは、appやprototypeの関数を直接書き換える「hijack(monkey-patch)」を非推奨としています。Comfyは開発が速く、書き換えた関数の前提が次の版で変わるためです。UIの追加は、用意されたhookとContext Menu APIの範囲で行います。

最小UI拡張を作る

JSは、PythonのpackageからWEB_DIRECTORYで公開します。現行ドキュメント(2026年8月時点)の形です。

# __init__.py
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS

WEB_DIRECTORY = "./js"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]

js/の中のJSは起動時にすべて読み込まれます。app../../scripts/app.jsから取り、registerExtensionに一意のnameとhookを渡します。

// js/my_node_ui.js
import { app } from "../../scripts/app.js";

app.registerExtension({
  name: "example.mynode.ui",
  async beforeRegisterNodeDef(nodeType, nodeData) {
    if (nodeData.name !== "MyNode") return;   // 自分のノードだけ
  },
  nodeCreated(node) {
    if (node.comfyClass !== "MyNode") return;
    node.addWidget("button", "reset", null, () => {
      const w = node.widgets.find((x) => x.name === "strength");
      if (w) w.value = 1.0;
    });
  },
});

この例がしているのは「自分のノードに、strengthを1.0に戻すボタンを足す」だけです。最初の拡張はこの程度に留め、動く・保存できる・再読込で戻る、の3つを確かめてから機能を増やします。

node.addWidgetはLiteGraph由来のノードAPIで、typenamevaluecallbackoptionsを持ちます。引数の細部は現行frontendの型定義で確認してください。公式ドキュメントは、hookの完全な一覧はapp.js内の#invokeExtensionsを検索して確認するよう案内しています。

ノードに依存しない設定値(表示の有無など)は、settingsで登録し、app.extensionManager.setting.get(id)で読みます。idは全拡張で一意である必要があります。

app.registerExtension({
  name: "example.mynode.settings",
  settings: [
    { id: "example.mynode.showHint", name: "MyNode: show hint", type: "boolean", defaultValue: false },
  ],
});
// 読み出し
if (app.extensionManager.setting.get("example.mynode.showHint")) { /* 表示 */ }

Widget値を同期する

ノードのwidgetの値は、graphの保存時にwidgets_valuesとして書き出され、再読込時に同じ順序で戻されます。公式ドキュメントは、widgets_valuesがpromptへのserializeに含まれると説明しています。つまり、保存に残したい値は「widgetとして存在する」ことが条件です。

{
  "id": 12,
  "type": "MyNode",
  "widgets_values": [1.0, "default"],
  "inputs": [],
  "outputs": []
}

ここで気をつける点が2つあります。

  • 順序に依存する。widgets_valuesは名前ではなく配列の順序で対応します。JSでwidgetを追加・削除すると順序がずれ、古いworkflowを読み込んだときに値が別のwidgetに入ります。追加するwidgetは末尾に置き、Python側のINPUT_TYPESの順序を変えないようにします
  • ボタンの値は保存しない。上の例のボタンはvaluenullで、保存に残す意味がありません。保存に残したくないwidgetは、配列に混ざらないようにするか、混ざっても害がない位置に置きます

JS側だけで持つ状態(折りたたみの開閉など)を保存したい場合は、widgetではなくnode.propertiesのような保存対象の領域に自分のkeyで入れ、Python側の入力と混ぜません。Backendが読む値はINPUT_TYPESで定義したものだけ、という境界を保ちます。

イベントを増やしすぎない

拡張は、起動時に全ノード・全graphに対して呼ばれるhookを持っています。beforeRegisterNodeDefはすべてのノード型に対して1回ずつ、nodeCreatedはすべてのノードのインスタンスに対して呼ばれます。自分のノード以外で何もしない、という早期returnを必ず入れます。

hook 呼ばれる単位 向く用途 避けること
init ページ読み込み時に1回 拡張内部の初期化 graphやnodeへの操作(まだ存在しない)
beforeRegisterNodeDef ノード型ごとに1回 自分のノード型の振る舞い調整 他ノード型への変更
nodeCreated ノードのインスタンスごと widgetの追加 重い処理・ネットワーク呼び出し
loadedGraphNode 読込時、ノードごと 古い保存形式の補正 値の上書き
setup 起動完了時に1回 イベントリスナーの登録 全イベントの購読

サーバーからのイベント(execution_startなど)はapi.addEventListenerで購読できますが、購読するのは自分の表示に必要なものだけにします。setupでcanvas全体のクリックやキー入力を拾うグローバルなリスナーを足すと、他の拡張と衝突し、原因の切り分けが難しくなります。右クリックメニューへの追加も、公式のgetNodeMenuItemsgetCanvasMenuItemsを使い、既存メニューの関数を書き換えません。

Nodes 2.0との互換性を確認する

現行UIにはVueベースの描画(Nodes 2.0)と従来のLiteGraph描画があり、メニューの「Toggle Nodes 2.0」で切り替えられます。公式ドキュメントは「Some custom nodes may require updates to be fully supported」と述べており、どのAPIが動かないかの列挙はありません。したがって、自分の拡張が両方で動くかは、自分で表示して確認するしかありません。

原則は「旧canvasの描画に依存しない」ことです。ノード上に直接線や文字を描く処理、canvasの座標を前提にしたクリック判定は、描画系が変わると動かなくなります。widgetの追加・設定値・メニュー項目のような、描画系に依存しない公式APIの範囲に留めれば、両方で動く可能性が高くなります。

同じノードを両UIで保存→再読込まで検証する

手順(両UIで同じことを行う)
1. ノードを配置し、追加したwidgetが表示されるか見る
2. widgetの値を変え、ボタンを押し、反応を見る
3. workflowをJSONに保存する
4. ページを再読込し、同じJSONを読み込む
5. widgetの値・順序・表示が保存前と一致するか見る
6. 実行し、Python側に渡った値をログで確認する
確認項目 旧UI(LiteGraph) Nodes 2.0 差があった場合の原因候補
widgetが表示される 描画依存のAPI
ボタンが反応する クリック判定の座標依存
保存JSONのwidgets_valuesが同じ widgetの順序・追加位置
再読込後の値が一致 loadedGraphNodeでの上書き
実行時にPython側が同じ値を受ける JS側だけの状態を送っている
consoleにエラーがない 存在しないプロパティへのアクセス

確認したfrontendのversionも表に書き添えます。frontendは独立して更新されるため、「いつの版で確認したか」がないと記録の意味が薄れます。

UI障害から処理を守る

JS側で例外が出ても、ユーザーがノードを実行できる状態を保ちます。守り方は、JS側とPython側の両方にあります。

  1. hookの中をtry/catchで包む。自分の拡張の例外で、他の拡張やノード登録を止めない。失敗したらconsoleに拡張名付きで出して黙る
  2. 存在を確認してから触る。node.widgetsや目的のwidgetが無い場合に備え、見つからなければ何もしない
  3. Python側で値を検証する。JSの補助が効かなくても、範囲外や型違いはINPUT_TYPESの制約と実行時チェックで止める
  4. JSを外して動かしてみる。WEB_DIRECTORYを一時的に無効にし、ノードが実行できることを確かめる。できなければ、JSに処理を持たせすぎている
nodeCreated(node) {
  try {
    if (node.comfyClass !== "MyNode") return;
    const w = node.widgets?.find((x) => x.name === "strength");
    if (!w) return;
    node.addWidget("button", "reset", null, () => { w.value = 1.0; });
  } catch (e) {
    console.warn("[example.mynode.ui] skipped:", e);
  }
}

「JSがなくても動く」を最後まで保てれば、frontendの更新で拡張が壊れても、ノードの利用者は手で値を入れて使い続けられます。UIの便利さは足し算で、データの正しさはPython側の引き算で守る、という分担です。

よくある質問

ノードの描画を丸ごと自分で描き替えたいのですが。

この記事の範囲では勧めません。描画の直接操作は旧canvasの前提に依存し、Nodes 2.0で動く保証がありません。widget・設定・メニューの公式APIで足りないか先に検討してください。

JS側で計算した値をPython側に渡せますか?

widgetの値として持たせれば、widgets_values経由でPython側の入力になります。ただし、その値はINPUT_TYPESで定義した入力に対応している必要があり、JSだけで作った隠し状態は渡りません。

hookの一覧はどこで確認できますか?

公式のjavascript_hooksページに主要なものが順序付きで載っています。公式自身が「追加のhookはapp.js#invokeExtensionsを検索して確認する」と案内しているため、最新はそこで確認してください。

まとめ

カスタムノードのJS UIは、WEB_DIRECTORYで公開したJSからapp.registerExtensionの公式hookに小さく載せます。値の正しさはPython側のINPUT_TYPESと実行時検証が持ち、JSは表示と補助に徹します。widgetの値はwidgets_valuesの順序で保存されるため、追加は末尾に置き、旧UIとNodes 2.0の両方で保存→再読込まで確かめてください。hijackは使わず、JSを外しても動く状態を保つことが、frontendの更新に耐える最小の条件です。

スポンサーリンク