makepad-2.0-performance について
Makepad 2.0のUI描画パイプラインとSplash VMの性能問題を調べる開発者向けスキルです。描画バッチ、テクスチャキャッシュ、ガベージコレクション、部分描画、診断ツールを解説します。
主な機能
- 01描画バッチと順序の解説
- 02テクスチャキャッシュの利用指針
- 03Splash VMのガベージコレクション
- 04対象サブツリーの描画最適化
- 05ログとGC診断
活用シーン
- 01背景に隠れるテキストの調査
- 02動的ビューの再描画負荷の削減
- 03長時間稼働アプリのメモリ回収の診断
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zhanghandong/ makepad-skills --skill makepad-2.0-performanceインストール後、Claude Code で/makepad-2.0-performanceを入力して読み込まれたか確認します。
同梱ファイルreferences/optimization-guide.md
詳細
Makepad 2.0 のパフォーマンスとデバッグに関するスキル
1. 概要
Makepad 2.0 は、独自のレンダリングパイプラインと Splash スクリプト VM を組み合わせて使用します。パフォーマンスを理解するには、次の 3 つの重要なサブシステムを把握する必要があります。
- 描画バッチ処理 - Makepad が GPU 描画呼び出しをどのようにまとめるか、また
new_batch: trueが重要な理由 - ガベージコレクション - Splash VM の、型ごとのバケットしきい値を使用するマーク・アンド・スイープ GC
- レンダリングトリガー - サブツリーをいつ再構築するかを制御する
on_render/.render()システム
従来の保持モード UI フレームワークとは異なり、Makepad は即時モードに着想を得た描画パイプラインを使用します。このパイプラインでは、ウィジェットが描画コマンドをソート済みのバッチリストに出力します。テキストが表示されない、ちらつく、パフォーマンスが低下するといった問題を診断するには、このパイプラインを理解することが不可欠です。
2. 描画バッチ処理システム
仕組み
Makepad は、同じシェーダーを使用する連続した描画呼び出しを、1 回の GPU 描画呼び出しに自動的にまとめます。これは大きなパフォーマンス最適化ですが、描画順序が予想外になるという重大な副作用があります。
Draw pipeline (simplified):
Widget tree: GPU batches (default):
View (bg shader) Batch 1: all bg shaders
Label (text) --> Batch 2: all text shaders
View (bg shader)
Label (text) Result: ALL backgrounds draw first,
then ALL text draws second
View に show_bg: true が設定され、かつテキストの子要素が含まれている場合、テキストが背景の後ろに配置されることがあります。テキストの描画がどちらも 1 回の描画呼び出しにまとめられ、その呼び出しが背景の描画呼び出しより前(または後)に実行されるためです。
new_batch: true
View に new_batch: true を設定すると、その時点で Makepad に新しい描画バッチを開始させることができます。これにより内部で ViewOptimize::DrawList が作成され、その View のサブツリー内で適切な描画順序が保たれます。
// PROBLEM: Label text is invisible - batched behind the background
RoundedView{
width: Fill height: Fit
draw_bg.color: #1e1e2e
Label{text: "This text is INVISIBLE"}
}
// FIX: new_batch ensures background draws before text
RoundedView{
width: Fill height: Fit
new_batch: true
draw_bg.color: #1e1e2e
Label{text: "This text is VISIBLE"}
}
new_batch: true が必要な場合
| シナリオ | 必須? | 理由 |
|---|---|---|
show_bg: true が設定され、Label を含む View | はい | テキストのバッチが背景の後ろに配置されます |
| ホバーアニメーターとテキストの子要素がある View | はい | 有効化時にホバー背景がテキストを覆います |
| 背景のある項目を繰り返し表示するコンテナー | はい | 各項目とコンテナーで必要です |
Label を含む透明な View(show_bg なし) | いいえ | 重なる背景がありません |
| テキスト以外の子要素(アイコンなど)のみを含む View | いいえ | シェーダーの種類が同じなので、重なりの問題はありません |
| 背景のある深くネストされた View | 各 View で必要 | 背景レイヤーごとに個別のバッチが必要です |
ホバー効果と new_batch
ホバー可能なリスト項目で最もよくある間違いです。View に show_bg: true が設定され、ホバー時に透明(#0000)から不透明へ遷移するホバーアニメーターがある場合、ホバー時に新たに不透明になった背景がバッチ化されたテキストを覆うため、テキストが消えます。
// CORRECT: Hoverable item with new_batch
let HoverItem = View{
width: Fill height: Fit
new_batch: true
show_bg: true
draw_bg +: {
color: uniform(#0000)
color_hover: uniform(#fff2)
hover: instance(0.0)
}
animator: Animator{
hover: {
default: {
from: {all: Forward{duration: 0.1}}
apply: {draw_bg: {hover: 0.0}}
}
on: {
from: {all: Forward{duration: 0.1}}
apply: {draw_bg: {hover: 1.0}}
}
}
}
label := Label{text: "item" draw_text.color: #fff}
}
// Parent container of hover items also needs new_batch
RoundedView{
flow: Down height: Fit new_batch: true
draw_bg.color: #2a2a3d
draw_bg.border_radius: 8.0
HoverItem{label.text: "First item"}
HoverItem{label.text: "Second item"}
}
ViewOptimize の内部動作
new_batch と texture_caching のプロパティは、ViewOptimize 列挙型に対応しています。
ViewOptimize::None - Default. No special draw ordering.
ViewOptimize::DrawList - Created by new_batch: true. Starts a new DrawList2d.
ViewOptimize::Texture - Created by texture_caching: true. Renders to offscreen texture.
優先順位:両方が設定されている場合、texture_caching が new_batch より優先されます。
3. テクスチャキャッシュ
仕組み
View に texture_caching: true を設定すると、子サブツリー全体がオフスクリーンの GPU テクスチャに描画されます。以降のフレームでサブツリー内に変更がなければ、Makepad は子要素の再描画を省略し、キャッシュされたテクスチャをブリットできます。
// Cache a complex but rarely-changing sidebar
sidebar := View{
width: 280 height: Fill
texture_caching: true
flow: Down spacing: 4
// ... many child widgets ...
}
事前定義済みのキャッシュ View
Makepad には、スタイルがあらかじめ設定されたキャッシュ View が用意されています。
| ウィジェット | 説明 |
|---|---|
CachedView | テクスチャキャッシュされた矩形コンテナー |
CachedRoundedView | テクスチャキャッシュされた角丸矩形 |
テクスチャキャッシュを使用する場合
適した例:
- ほとんど変更されない、複雑で静的なサイドバーやツールバー
- ネストされた背景やテキストを多数含む、大きなウィジェットサブツリー
- シェーダー効果を使った装飾パネル
適さない例:
- 頻繁に更新される View(アニメーションの対象やライブデータなど)
- シンプルで小さな View(オーバーヘッドが利点を上回ります)
- スクロール可能なコンテンツを含む View(CachedView は表示領域ではなく、コンテンツ全体をラップします)
トレードオフ
| 利点 | コスト |
|---|---|
| フレームごとの描画呼び出し数を削減します | キャッシュ用テクスチャに GPU メモリを使用します |
| 大きなサブツリーを再走査せずに済みます | 変更時にテクスチャを無効化する必要があります |
| バッチ処理の問題を解消できる場合があります(テクスチャによって描画順序が解決されます) | DPI 係数がテクスチャの解像度に影響します |
4. ガベージコレクション(mod.gc)
アーキテクチャ
Splash VM は、値の種類ごとに分離されたヒープを持つマーク・アンド・スイープ型ガベージコレクターを使用します。
Heap Layout:
+-- Objects (ScriptObject) -- Primary allocation type
+-- Arrays (ScriptArray) -- Typed arrays and value arrays
+-- Strings (ScriptString) -- Interned strings
+-- Pods (ScriptPod) -- Pod values (vec2, vec3, vec4, etc.)
+-- Handles (ScriptHandle) -- Native Rust handles
+-- Regexes (ScriptRegex) -- Interned regex patterns
GC の自動起動
GC は、Lua や V8 と同様の増加率ベースのヒューリスティックを使用します。
- 増加率: 2 倍 — 前回の GC 以降にいずれかのヒープカテゴリが 2 倍になると、GC が起動します
- 最小しきい値(小さなヒープでの過剰な GC 起動を防ぐため):
| カテゴリ | GC 起動可能になる最小値 |
|---|---|
| オブジェクト | 1,024 |
| 文字列 | 256 |
| 配列 | 128 |
| Pod | 128 |
| ハンドル | 64 |
GC は次の条件で起動します:current_count >= MIN_THRESHOLD AND current_count >= last_gc_count * 2
スクリプト API
mod.gc.run() — GC サイクルを直ちに強制実行します。サイレントに実行されます(ログ出力はありません)。
mod.gc.run_status() — GC サイクルを強制実行し、詳細な統計情報を出力します。
GC 142us: obj[S:1200 A:340 R:89] arr[S:45 A:12 R:3] str[S:890 A:120 R:15] ...
S は静的(永続)、A は生存(保持)、R は削除(解放)を表します。
mod.gc.set_static(value) — 値と、その値から到達可能なオブジェクトグラフ全体を静的としてマークします。静的オブジェクトは次のとおりです。
- GC によって収集されません(永続します)
- GC のマークフェーズではスキップされます(GC の走査が高速になります)
- マークを解除できません(VM の存続期間中は元に戻せません)
mod.gc.dump_tag(value) — デバッグツールです。オブジェクトの内部タグ情報(型インデックス、静的フラグ、プロトチェーン)を出力します。
ベストプラクティス
パターン:静的 UI ツリー
多数のタブを持つ Dock など、大きくて安定した UI ツリー定義は、定義直後に静的としてマークします。これは Studio と UIZoo の例で使われている標準的なパターンです。
// Define a large widget tree
let AppDock = Dock{
// ... tabs, splitters, content templates ...
TabEditor := TabEditor{}
TabFileTree := TabFileTree{}
TabSettings := TabSettings{}
}
// Mark the entire tree as static - it will never be GC'd
mod.gc.set_static(AppDock)
// Run GC immediately to clean up any temporaries from tree construction
mod.gc.run()
// Now start the app
startup() do #(App::script_component(vm)){
ui: Root{
main_window := Window{
body +: {
// ... use AppDock here ...
}
}
}
}
パターン:動的コンテンツ
リスト、ユーザー生成項目、チャットメッセージなどの動的コンテンツは、自動 GC によるクリーンアップに任せます。
// Dynamic data - no need to call mod.gc manually
var todos = []
fn add_todo(text) {
todos.push({text: text done: false})
ui.main_view.render()
// Automatic GC will clean up old unreachable objects
}
fn delete_todo(index) {
todos.splice(index, 1)
ui.main_view.render()
// Old todo object becomes unreachable, will be collected automatically
}
パターン:長時間実行するアプリでの定期的な手動 GC
ストリーミング応答のあるチャットアプリなど、多数のオブジェクトを作成および破棄するアプリの場合:
var message_count = 0
fn on_new_message(msg) {
messages.push(msg)
message_count += 1
// Every 100 messages, run GC to reclaim temporary parsing objects
if message_count % 100 == 0 {
mod.gc.run()
}
ui.message_list.render()
}
GC マークフェーズの詳細
マークフェーズでは、次のルートから走査します。
- 型チェック用プロトタイプ
- 型のデフォルトオブジェクト
- Pod 型のデフォルトとオブジェクト
- ルートオブジェクト(Rust が
ScriptObjectRefを介して保持) - ルート配列(Rust が
ScriptArrayRefを介して保持) - ルートハンドル(Rust が
ScriptHandleRefを介して保持) - スレッドスタック(VM の実行スタック上にあるすべての生存値)
- スレッドスコープ
- メソッド呼び出しコンテキスト
- ループソースの値
- トラップのエラー/戻り値/中断値
- スクリプト本体のスコープとトークナイザーの文字列リテラル
- ネイティブ型テーブルのオブジェクト
静的オブジェクトは、他の静的な値のみを参照するため、走査中はスキップされます。
5. レンダリングの最適化
on_render / .render() システム
Makepad 2.0 は、動的コンテンツにプル型レンダリングモデルを使用します。View の on_render コールバックは、その View に対して .render() が呼び出されたときにのみ実行されます。
// Define a reactive view
counter_view := View{
on_render: || {
Label{
text: "Count: " + state.counter
draw_text.color: #fff
}
}
}
// In event handler - only re-render what changed
fn increment() {
state.counter += 1
ui.counter_view.render() // Only this view re-renders
}
効率的なレンダリングのルール
-
不要な
.render()の呼び出しは絶対に避けてください — 呼び出すたびに、そのサブツリーのウィジェット出力が完全に再構築されます。 -
影響を受けたサブツリーのみをレンダリングしてください — 変更されたのがリストだけなら、UI 全体ではなくリストビューのみをレンダリングします。
-
タイトなループ内でのレンダリングは避けてください — 状態変更をまとめてから、一度だけレンダリングします。
// BAD: renders 100 times
for i in 0..100 {
items[i].value = compute(i)
ui.item_list.render() // WASTEFUL - rebuilds list 100 times
}
// GOOD: render once after all changes
for i in 0..100 {
items[i].value = compute(i)
}
ui.item_list.render() // Render once with all changes applied
- 初回レンダリングには
on_startupを使用してください — アプリの起動時に最初のレンダリングを実行します。
ui: Root{
on_startup: || {
ui.main_view.render()
}
main_window := Window{
body +: {
main_view := View{
on_render: || {
// ... dynamic content ...
}
}
}
}
}
レンダリングのスコープ
View に対して .render() が呼び出されると、その View の on_render コールバックのみが実行されます。独自の on_render コールバックを持つ子 View は、子 View 自身に対して .render() が呼び出されるか、親の on_render によって再構築されない限り、自動的には再レンダリングされません。
6. デバッグログ
Rust 側のログ出力
Makepad のエラーログシステムが提供する log! マクロを使用します。
use makepad_widgets::*;
// In Rust code
log!("Button clicked, counter = {}", self.counter);
log!("Widget action: {:?}", action);
スクリプト側のログ出力
Splash スクリプトでは、デバッグに log() または文字列補間を使用できます。
fn handle_click() {
let value = compute_something()
// Log values during development
log("computed value: " + value)
}
GC ステータスのログ記録
mod.gc.run_status() を使って、GC アクティビティの詳細な内訳を取得します。
// Output example:
// GC 142us: obj[S:1200 A:340 R:89] arr[S:45 A:12 R:3] str[S:890 A:120 R:15]
// hdl[S:8 A:2 R:0] pod[S:200 A:45 R:10] rex[S:3 A:0 R:0]
フィールド:
- Time(142us)- GC サイクルの所要時間(マイクロ秒)
- S(Static)- 永続的にマークされ、回収されないオブジェクト
- A(Alive)- この GC サイクルで生存したオブジェクト
- R(Removed)- このサイクルで解放されたオブジェクト
タグのデバッグ
特定のオブジェクトを詳しくデバッグするには、mod.gc.dump_tag(value) を使います。
let my_widget = View{...}
mod.gc.dump_tag(my_widget)
// Output: obj 4523 type_index=Some(12) is_static=false proto=Some(89) ...
7. よくあるパフォーマンスの問題と対処法
| 問題 | 原因 | 対処法 |
|---|---|---|
| テキストが表示されない | new_batch がない | 親の View に show_bg: true とともに new_batch: true を追加する |
| ホバー時にテキストが消える | ホバーアニメーション中のバッチの重なり | ホバー可能な View に new_batch: true を追加する |
| UI がフリーズする/カクつく | .render() の呼び出し過多 | 状態変更をまとめ、変更されたサブツリーのみをレンダリングする |
| メモリ使用量が際限なく増える | GC が実行されていない、または大きな静的リーク | 安定したツリーには mod.gc.set_static() を使い、動的コンテンツは自動 GC に任せる |
| 初回読み込みが遅い | 起動時の大規模なスクリプト評価 | モジュールに分割し、遅延読み込みのパターンを使う |
| スクロールがカクつく | 描画するアイテムが多すぎる | 仮想化描画に PortalList を使う |
| ホバーに反応しない | View に show_bg: true がない | ホバー時のマウスイベントを受け取るには、View に show_bg: true が必要 |
| 実行時に Widget が見つからない | 命名演算子が間違っている | 名前付き/アドレス指定可能な子には : ではなく := を使う |
| スタイルの上書きが適用されない | マージ演算子がない | プロパティのマージには : ではなく +: を使う。: はすべて置き換える |
| レイアウトの高さがゼロになる | height: Fit がない | すべてのコンテナーに height: Fit または固定の高さが必要 |
8. 大きなリスト向けの PortalList
PortalList を使う理由
PortalList は描画を仮想化し、ビューポートに表示されているアイテムだけを描画します。アイテムが 100 個を超えるリストでは必須です。使用しない場合、表示されているかどうかにかかわらず、毎フレームすべてのアイテムが描画されます。
スクリプト側の PortalList(on_render を使用)
Splash 主導のリストでは、テンプレートを使って PortalList を定義し、on_render を使います。
list := PortalList{
width: Fill height: Fill
flow: Down spacing: 4
scroll_bar: ScrollBar{}
Item := View{
width: Fill height: Fit
padding: 8
new_batch: true
draw_bg.color: #2a2a3d
label := Label{text: "" draw_text.color: #ddd}
}
}
Rust 側の PortalList(Widget トレイトを使用)
Rust 主導の描画では、Widget トレイトを実装します。
impl Widget for MyList {
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
while let Some(item) = self.view.draw_walk(cx, scope, walk).step() {
if let Some(mut list) = item.borrow_mut::<PortalList>() {
list.set_item_range(cx, 0, self.items.len());
while let Some(item_id) = list.next_visible_item(cx) {
let template = id!(Item);
let item = list.item(cx, item_id, template);
item.label(ids!(label)).set_text(cx, &self.items[item_id].text);
item.draw_all(cx, &mut Scope::empty());
}
}
}
DrawStep::done()
}
}
FlatList と PortalList の比較
| 機能 | FlatList | PortalList |
|---|---|---|
| 仮想化 | いいえ | はい |
| 適した用途 | 100 個未満のアイテム | アイテム数を問わない |
| メモリ使用量 | すべてのアイテムをメモリに保持 | 表示中のアイテムのみ |
| スクロールのパフォーマンス | アイテム数に応じて低下 | 一定 |
9. ViewOptimize オプションの概要
| プロパティ | ViewOptimize の値 | 効果 |
|---|---|---|
| (デフォルト) | None | 標準のバッチ描画 |
new_batch: true | DrawList | 新しい描画バッチを作成し、適切な描画順序を確保 |
texture_caching: true | Texture | 子要素をオフスクリーンテクスチャに描画 |
visible: false | N/A | 描画を完全にスキップ |
優先順位: texture_caching と new_batch の両方が設定されている場合は、texture_caching が優先されます(ViewOptimize::Texture になります)。
10. デバッグ用チェックリスト
UI が表示されない
- すべてのコンテナーで
height: Fitを確認する(Fitの親の内側でデフォルトのheight: Fillを使うと高さが 0 になります) - ルートコンテナーで
width: Fillを確認する(最外部の要素に固定ピクセル幅を使わないでください) - スクリプトの先頭に
use mod.prelude.widgets.*があることを確認する
テキストが表示されない
- テキストを含む
show_bg: trueの View にnew_batch: trueを追加する -
draw_text.colorが透明でも背景と同色でもないことを確認する - Label が直接の子、または適切にバッチ化された子であることを確認する
ホバー時にテキストが消える
- ホバー可能な View に
new_batch: trueを追加する - ホバー可能なアイテムのコンテナーにも
new_batch: trueが設定されていることを確認する
クリックが機能しない
-
:=と:の違いを確認する。参照する名前付き/動的な子には:=を使う - マウスイベントが必要な View に
show_bg: trueが設定されていることを確認する - キーボードイベントが必要な場合は
grab_key_focusを確認する
ウィジェットが見つからない
- script_mod で
#(WidgetName::register_widget(vm))が登録されていることを確認します - カスタム登録より前に
crate::makepad_widgets::script_mod(vm)が呼び出されていることを確認します - ウィジェットのクレートが Cargo.toml の依存関係に含まれていることを確認します
スクリプトエラー
- 実行中の値をデバッグするには
log()を使用します - ヒープ統計を確認するには
mod.gc.run_status()を使用します - オブジェクトの内部を調べるには
mod.gc.dump_tag(value)を使用します
スタイルが適用されない
- 既存のスタイルを拡張するには、
+:マージ演算子を使用します:draw_bg +: { color: #fff } - プロパティを完全に置き換える場合にのみ
:を使用します - ドットパス構文を確認します:
draw_bg.color: #fffはdraw_bg +: { color: #fff }の省略形です
11. Studio によるプロファイリング
Makepad Studio には、アプリケーションのパフォーマンスを監視するためのプロファイラーが組み込まれています。
Studio リモートプロトコル
Studio は実行中のアプリケーションに接続し、次の機能を提供します:
- スクリーンショット - 現在のフレームの状態をキャプチャします
- ウィジェットツリーダンプ - 実行中のウィジェット階層を調べます
- ウィジェットクエリ - ID で特定のウィジェットを検索します
- パフォーマンス監視 - フレーム時間、描画呼び出し数
パフォーマンスのデバッグに Studio を使用する
- Studio リモートを起動します:
cargo run -p cargo-makepad --release -- studio --studio=127.0.0.1:8001
- Studio 経由でアプリを実行します:
{"Run":{"mount":"makepad","process":"makepad-example-myapp","args":[]}}
- ウィジェットツリーをキャプチャして、レンダリング構造を確認します:
{"WidgetTreeDump":{"build_id":BUILD_ID}}
- スクリーンショットを撮って、表示状態を確認します:
{"Screenshot":{"build_id":BUILD_ID}}
負荷の高いレンダリングパスを特定する
WidgetTreeDumpを使って、ツリー内のウィジェット数を確認しますtexture_cachingが有効な、深くネストされた View 階層を探します- 手動ループではなく
PortalListを使用すべき繰り返し項目を特定します new_batch: trueが設定された View を確認し、不要なものがないか調べます(新しいバッチごとに新しい描画リストが作成されます)
12. クイックリファレンスカード
View のパフォーマンスプロパティ
// Force new GPU draw batch (fixes text-behind-background)
new_batch: true
// Cache children to GPU texture (reduces draw calls for stable subtrees)
texture_caching: true
// Hide without removing from tree (skip rendering entirely)
visible: false
GC API
mod.gc.set_static(value) // Mark value tree as permanent
mod.gc.run() // Force GC cycle (silent)
mod.gc.run_status() // Force GC cycle with log output
mod.gc.dump_tag(value) // Debug: print object tag info
レンダリング API
ui.widget_name.render() // Trigger on_render for specific widget
GC のしきい値(自動トリガー)
Objects: >= 1024 AND >= 2x since last GC
Strings: >= 256 AND >= 2x since last GC
Arrays: >= 128 AND >= 2x since last GC
Pods: >= 128 AND >= 2x since last GC
Handles: >= 64 AND >= 2x since last GC