10.1 性能优化
> **本节目标**:理解 RML 基于 GPUI 的真实渲染机制,掌握 `#[computed]` 缓存、`#[command(no_notify)]` 选择性 notify、element ID 稳定性等优化手段。
10.1.1 GPUI 渲染模型
RML 建立在 GPUI 之上,渲染流程如下:
cx.notify()
│
▼
标记 Entity 为脏(app.notify(entity_id))
│
▼
下一次 frame:整个 render() 方法重新执行
│
▼
重建整棵 element 树(所有 format!()、.child() 重新求值)
│
▼
GPUI element ID intern:相同 ID 的元素复用 layout/paint 状态
│
▼
GPU 合成帧
**关键事实**: - `cx.notify()` 触发**整个** `render()` 重建,无法跳过子树 - `AnyElement` 是 `ArenaBox`,frame 结束时释放,**无法跨 frame 缓存 element** - GPUI 的 element ID intern 机制会缓存 layout/paint 状态(非 render 本身),这是内置优化 - `#[computed]` 缓存的是**数据**(计算结果),不是 element
性能瓶颈通常出现在: 1. **notify 过频**:每次按键都全量重建 2. **render 内重计算**:`#[computed]` 未使用,昂贵计算重复执行 3. **element ID 不稳定**:GPUI 无法复用 layout/paint 状态
10.1.2 `#[computed]` 数据缓存
`#[computed]` 通过版本号追踪依赖,只在依赖字段变化时重算。这是 RML 最重要的性能优化手段。
#[window]
#[derive(Default)]
pub struct MainWindow {
pub count: i32,
pub items: Vec<Item>,
}
impl MainWindow {
/// 只在 count 或 items 变化时重算
#[computed]
pub fn summary(&self) -> String {
format!("共 {} 项,计数 {}", self.items.len(), self.count)
}
}
**缓存机制**: - 每个 `pub` 字段在 `__rml_state.field_versions: HashMap<String, AtomicU64>` 中惰性插入版本号计数器 - `#[command]` 修改字段时自动 `bump_version` - `#[computed]` 方法调用时检查依赖字段版本号之和,未变则返回缓存值
**优化要点**: - 依赖范围最小化:`#[computed]` 只访问必要的字段 - 避免在 `#[computed]` 中访问整个 `Vec`(任何元素变化都触发重算)
双向绑定的性能特征
`<input value={field}>` 的双向绑定基于 `Entity<InputState>` + `cx.subscribe`,其性能开销分层:
| 操作 | 时机 | 开销 | 备注 | |---|---|---|---| | `Entity<InputState>` 创建 | 首次 render | 一次性 | 后续 render 复用,存入 `__rml_state.input_states` | | `cx.subscribe` + `.detach()` | 首次 render | 一次性 | 订阅随 entity 存活,不占结构体字段 | | 版本号对比 | 每次 render | O(1) | `AtomicU64::load` + 整数比较 | | `InputState::set_value` | 仅版本号变化时 | 字符串拷贝 | `emit_events=false` 不触发反向闭包 | | 反向闭包执行 | 用户每次输入 | 字段回写 + `HashMap::insert` | 闭包内 `bump_version` + `cx.notify()` | | `__rml_state.field_errors.get(field)` | 每次 render | O(1) HashMap 查询 | Phase B-3.1 校验状态查询,确定是否包裹红色边框 + tooltip |
**关键优化点**:
1. **惰性初始化**:`InputState` entity 仅在首次 render 时创建,后续 render 仅版本号对比 2. **正向同步条件触发**:`__rml_get_version(field) != __rml_state.input_state_versions[field]` 时才调用 `set_value`,避免冗余更新 3. **`Subscription.detach()`**:不存储 `Vec<Subscription>`(避免 `Subscription` 非 `Sync` 导致视图不满足 `Send + Sync`),订阅随 entity 生命周期自动清理 4. **`cx.notify()` 批量合并**:反向闭包内的 `cx.notify()` 与 `#[command]` 的自动 notify 会合并,多次输入只触发一次 render
**性能瓶颈场景**:
- **大量 `<input value={field}>`**:每个绑定创建独立 `Entity<InputState>` + 订阅,N 个输入框 = N 个 entity + N 个订阅。对于 10+ 输入框的表单,首次 render 会有明显开销 - **高频输入**:每次按键触发 `InputEvent::Change` → 反向闭包 → `cx.notify()` → 全量 render 重建。对于复杂界面,考虑防抖 - **`#[command]` 修改双向绑定字段**:触发正向同步 `set_value`,虽然 `emit_events=false` 避免循环,但仍有字符串拷贝开销
校验规则执行开销
`#[validate]` 声明的校验规则在反向绑定(用户输入)时执行,不影响正向渲染性能:
| 规则 | 执行时机 | 复杂度 | 备注 | |---|---|---|---| | `required` | 反向闭包内 | O(1) | `String::is_empty()` 检查 len,开销可忽略 | | `length(min, max)` | 反向闭包内 | O(1) | `len()` 直接读取存储的长度 | | `range(min, max)` | 反向闭包内 | O(1) | 数值比较,开销可忽略 | | `regex = "..."` | 反向闭包内 | O(n) | `regex` crate 编译 + 匹配;每次输入重复编译 | | `custom = "fn"` | 反向闭包内 | 取决于函数 | 用户实现的函数复杂度 |
**关键特点**:
- **仅反向执行**:校验规则在 `InputEvent::Change` 闭包内执行,正向同步(`set_value`)不触发校验 - **短路求值**:if-else if 链任一规则失败即停止,不执行后续规则 - **regex 编译开销**:每次校验调用 `rml::regex::Regex::new(pattern).unwrap()`,未缓存编译结果。高频输入场景可考虑用 `custom` 函数 + `std::sync::OnceLock` 缓存编译后的 `Regex` - **整体影响极小**:校验仅在用户输入时执行(非每次 render),相比 `cx.notify()` 触发的全量 render 重建,校验开销可忽略
**regex 优化示例**(高频输入场景):
use std::sync::OnceLock;
use rml::regex::Regex;
impl Form {
// 缓存编译后的 regex,避免每次校验重复编译
fn email_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new(r"^\w+@\w+\.\w+$").unwrap())
}
fn validate_email(value: &str) -> Option<SharedString> {
if !Self::email_regex().is_match(value) {
Some("邮箱格式错误".into())
} else {
None
}
}
}
// 用 custom 替代 regex 规则,避免重复编译
#[validate(custom = "validate_email")]
pub email: String,
**优化建议**:
// ❌ 反例:高频输入触发全量 render
<input value={search_text} />
// 每次按键 → InputEvent::Change → cx.notify() → render 重建
// ✅ 正例:防抖搜索(value 双向同步 + 防抖触发搜索)
<input value={search_text} />
<button on-click={perform_search}>搜索</button>
// 用户点击按钮才触发搜索,而非每次输入
10.1.3 `#[command(no_notify)]` 选择性 notify
默认情况下,`#[command]` 自动注入 `cx.notify()`。但有些场景不需要立即更新 UI:
// 默认:自动 notify(即时反馈)
#[command]
pub fn on_click(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.count += 1;
// 宏自动注入:self.__rml_bump_version("count"); cx.notify();
}
// 不自动 notify(批量操作)
#[command(no_notify)]
pub fn batch_update(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.a = 1;
self.b = 2;
self.c = 3;
// 宏注入 bump_version 但不注入 notify
cx.notify(); // 手动调用一次,而非三次
}
**适用场景**: - 批量操作:多个字段修改后只需一次 notify - 后台更新:数据加载中,UI 不需即时更新 - 条件更新:根据业务逻辑决定是否 notify
**`__rml_changed_fields()` 方法**:返回所有 observable 字段名,供手动判断:
#[command(no_notify)]
pub fn conditional_update(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.count += 1;
// 只在 count > 10 时更新 UI
if self.count > 10 {
cx.notify();
}
}
10.1.4 Element ID 稳定性
GPUI 的 element ID intern 机制要求元素有稳定 ID。RML 通过 `ref` 指令提供:
<div ref="list_container">
<h1 ref="title">{title}</h1>
<Button ref="submit_btn" on-click={on_submit} label="提交" />
</div>
**生成代码**:
// ref="title" → .id("rml_ref:title")
gpui::div().id("rml_ref:title").child(format!("{}", self.title))
**优化要点**: - 为有状态的元素(事件监听器、动画)添加 `ref` - 列表渲染时用 `each` 指令的 item 变量生成唯一 ID - 避免 `if` 频繁切换元素类型(破坏 ID 复用)
10.1.5 避免过度 notify
反例:循环中 notify
#[command]
pub fn load_batch(&mut self, items: Vec<Item>, cx: &mut Context<Self>) {
for item in items {
self.items.push(item);
// ❌ 每次 push 都触发 notify(#[command] 自动注入)
}
}
正例:批量修改 + 单次 notify
#[command(no_notify)]
pub fn load_batch(&mut self, items: Vec<Item>, cx: &mut Context<Self>) {
self.items.extend(items);
cx.notify(); // ✅ 单次 notify
}
反例:高频输入 notify
// 每次 on_change 都触发全量 render 重建
#[command]
pub fn on_input(&mut self, state: &InputState, cx: &mut Context<Self>) {
self.query = state.value();
// #[command] 自动注入 cx.notify()
self.refresh_results(cx); // 又一次 notify
}
正例:防抖搜索
#[command(no_notify)]
pub fn on_input(&mut self, state: &InputState, cx: &mut Context<Self>) {
self.query = state.value();
cx.notify(); // 更新输入框显示
self.schedule_debounced_search(cx); // 内部防抖,延迟 notify
}
10.1.6 异步任务调度
重计算应放到后台线程,避免阻塞渲染:
#[command]
pub fn analyze(&mut self, cx: &mut Context<Self>) {
let data = self.data.clone();
self.is_analyzing = true;
// #[command] 自动注入 notify
cx.spawn(|this, mut cx| async move {
let result = cx.background_executor()
.spawn(async move { heavy_compute(&data) })
.await;
let _ = this.update(&mut cx, |this, cx| {
this.result = result;
this.is_analyzing = false;
cx.notify();
});
}).detach();
}
10.1.7 性能优化清单
- [ ] 昂贵计算用 `#[computed]` 缓存 - [ ] 批量操作用 `#[command(no_notify)]` + 手动单次 notify - [ ] 有状态的元素添加 `ref` 指令(稳定 element ID) - [ ] 循环中不触发 notify - [ ] 高频输入考虑防抖 - [ ] 重计算放到后台线程 - [ ] `#[computed]` 依赖范围最小化 - [ ] 列表渲染用 `each` + 稳定 key
10.1.8 性能调试
检查 `#[computed]` 缓存命中
在 `#[computed]` 方法中加日志:
#[computed]
pub fn summary(&self) -> String {
eprintln!("summary recomputed"); // 版本号变化时才打印
format!("共 {} 项", self.items.len())
}
如果日志打印次数多于预期,说明依赖范围过宽。
检查 notify 频率
#[command(no_notify)]
pub fn debug_notify(&mut self, cx: &mut Context<Self>) {
self.count += 1;
eprintln!("before notify");
cx.notify();
eprintln!("after notify");
}
GPUI 内置性能工具
# 启用 GPUI 的帧调试
GPUI_FRAME_DEBUG=1 cargo run -p rust-rml-demo
下一节 → [10.2 调试技巧](./debugging.md)