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_DIRECTORYとregisterExtensionによる最小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で、type・name・value・callback・optionsを持ちます。引数の細部は現行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の順序を変えないようにします - ボタンの値は保存しない。上の例のボタンは
valueがnullで、保存に残す意味がありません。保存に残したくない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全体のクリックやキー入力を拾うグローバルなリスナーを足すと、他の拡張と衝突し、原因の切り分けが難しくなります。右クリックメニューへの追加も、公式のgetNodeMenuItems・getCanvasMenuItemsを使い、既存メニューの関数を書き換えません。
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側の両方にあります。
- hookの中をtry/catchで包む。自分の拡張の例外で、他の拡張やノード登録を止めない。失敗したらconsoleに拡張名付きで出して黙る
- 存在を確認してから触る。
node.widgetsや目的のwidgetが無い場合に備え、見つからなければ何もしない - Python側で値を検証する。JSの補助が効かなくても、範囲外や型違いは
INPUT_TYPESの制約と実行時チェックで止める - 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の更新に耐える最小の条件です。