Skip to content

Stage05|Performance ​

Lesson061|Node 和 Component 太多为什么会慢? ​

Tags: #Creator2.x #Stage05 #Node #Component #CPUDifficulty: ⭐⭐⭐☆☆


真实问题:节点全部不可见,页面为什么仍然慢 ​

一个活动页有 5000 个 Node,其中大部分 Sprite 被禁用,但脚本、Widget、Animation 和事件组件仍在节点树中。开发者认为“不渲染就没有成本”,实际打开、遍历和销毁仍然很慢。

节点数量影响的不只 DrawCall:

text
创建/反序列化
父子关系维护
active 状态传播
生命周期回调
Component 调度
Transform Dirty
事件和引用
销毁/GC

一、本课目标 ​

Node 和 Component 的成本不只在渲染。即使是空节点,也可能带来层级、Transform、激活状态、事件和生命周期管理成本。


二、一个 Node 可能带来的工作 ​

text
父子关系维护
Transform 传播
activeInHierarchy 计算
Component 管理
事件派发
生命周期
渲染数据

空节点不产生 DrawCall,但仍可能增加 CPU 遍历和状态管理成本。


三、Component 的成本 ​

每个组件可能带来:

  • 生命周期注册。
  • enabled 状态判断。
  • update / lateUpdate 调度。
  • 事件和定时器。
  • 渲染或布局工作。
  • 引用和内存占用。

组件化的原则是按职责拆分,不是把每一行逻辑都拆成一个组件。


四、节点树过深的问题 ​

text
Root
└── A
    └── B
        └── C
            └── D
                └── Item

父节点变化时,子树可能需要传播 Transform 或激活状态。层级过深还会增加:

  • 查找和遍历成本。
  • Layout 传播范围。
  • 调试和引用复杂度。

不是所有层级都必须扁平化,但应避免无意义的包装节点。


五、空 update 的成本 ​

ts
update() {}

即使函数体为空,也可能进入调度集合、每帧被判断和调用。低频逻辑优先使用事件、schedule 或集中管理器。


六、节点数量和渲染数量不是一回事 ​

text
Node 很多,不一定 DrawCall 很多
DrawCall 很多,也不一定 Node 很多

应分别测量:

  • Node / Component 数量。
  • 可调度组件数量。
  • 渲染组件数量。
  • DrawCall 和 Overdraw。
  • Layout / Transform 更新时间。

七、案例:复杂列表 ​

一个 Item 包含:

text
ItemRoot
├── Background
├── Icon
├── NameLabel
├── CountLabel
├── Button
├── RedPoint
└── Animator

100 个 Item 可能带来数百个 Node 和大量组件。优化方向可以是:

  • 只创建可见 Item。
  • 对 Item 使用对象池。
  • 合并不必要的装饰节点。
  • 低频更新数量和倒计时。
  • 避免每个 Item 都有空 update。

八、常见误区 ​

误区一:节点越少越好 ​

过度合并会损害复用、布局和可维护性。

误区二:空节点没有成本 ​

不渲染不等于不参与引擎管理。

误区三:所有组件都应该集中到一个脚本 ​

这会形成巨型脚本和高耦合,优化应基于测量。

误区四:节点数量就是卡顿根因 ​

节点只是候选因素,需要结合 CPU、布局、调度和渲染数据判断。


九、练习与答案 ​

  1. 空 Node 和空 Component 分别有什么成本?
  2. 为什么复杂列表适合对象池和只创建可见项?
  3. 如何判断 Node 数量是否是当前瓶颈?

答案:

  1. Node 有层级、Transform、状态和事件管理成本;Component 还有生命周期和调度成本。
  2. 减少创建销毁、节点数量和每帧更新范围。
  3. 通过 Profiler 对比遍历、调度、布局和渲染成本,而不是只看数量。

节点成本要按生命周期统计 ​

创建阶段 ​

Prefab 反序列化、Node/Component 分配、属性恢复、onLoad/onEnable。

稳定运行 ​

update、Scheduler、Animation、Transform、布局、事件、渲染收集。

状态切换 ​

父节点 active 改变会传播到后代并触发生命周期。

销毁 ​

onDisable/onDestroy、监听清理、引用断开和内存回收。

只测稳定 FPS 会漏掉打开/关闭尖峰。

Creator 2.4.x 实验:同数量不同结构 ​

NodeCountProbe.ts ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class NodeCountProbe extends cc.Component {
    @property(cc.Prefab)
    itemPrefab: cc.Prefab = null;

    @property(cc.Node)
    container: cc.Node = null;

    create(count: number) {
        const begin = Date.now();
        for (let i = 0; i < count; i++) {
            const node = cc.instantiate(this.itemPrefab);
            this.container.addChild(node);
        }
        cc.log('[node-count] create ms=', Date.now() - begin);
    }

    toggleRoot() {
        const begin = Date.now();
        this.container.active = !this.container.active;
        cc.log('[node-count] toggle submit=', Date.now() - begin);
    }

    clear() {
        const begin = Date.now();
        this.container.removeAllChildren();
        cc.log('[node-count] clear submit=', Date.now() - begin);
    }
}

removeAllChildren 的具体销毁/移除语义需按 API 选择;正式实验应明确是 remove、destroy 还是入池,不能混为一谈。

对照 ​

  1. Item 只有 Node。
  2. Item 增加一个空 Component。
  3. Component 增加空 update。
  4. Item 增加 Sprite/Label/Widget。
  5. 相同节点数分别做扁平结构和深层结构。
  6. 测创建、稳定、父 active 切换和销毁四段。

记录目标设备和 Release/Debug 配置。

空 Component 为什么也不是零成本 ​

Component 可能带来:

  • 反序列化和字段;
  • 生命周期函数;
  • enabled 状态;
  • 调度注册;
  • 事件和引用;
  • 脚本对象内存。

空 update 的单次成本很小,但大量实例会放大。更重要的是,有实际逻辑的 Component 往往还进行搜索、分配和跨系统调用。

真实项目故障:3000 个排行榜 Item 预创建 ​

团队为避免滚动时创建,启动时预创建全部 Item 并设为 inactive。

代价:

text
启动 instantiate 峰值
→ 数千 Node/Component 常驻
→ 大对象池保留 Label/SpriteFrame
→ active 切换传播
→ 内存高且首屏慢

修复为虚拟列表,只保留可见区加缓冲的 20~30 个 Item;数据数组仍保留 3000 条,但渲染对象不等于数据数量。

诊断与练习答案 ​

节点数高但 CPU 稳定 ​

仍检查创建/销毁峰值和内存。静态节点可能稳定便宜,但不代表生命周期成本可忽略。

节点数低却很慢 ​

单个 Spine/Graphics/粒子、复杂脚本或全屏 Shader 可能很贵;Node Count 不是总分。

  1. inactive Node 是否完全无成本? 不渲染/通常不更新,但仍占对象、组件和资源引用,切 active 与销毁也有成本。
  2. 为什么数据列表不应一一对应常驻 Item? 可用虚拟化将大量数据映射到少量可见渲染节点。
  3. 扁平结构一定更快吗? 不一定。它可能减少父 Dirty 深度,但增加管理和重复;需按实际访问与更新测试。
  4. 对象池为何要设容量? 池中 Node/Component/资源仍占内存,过大池把尖峰问题变成长驻问题。

十、本课总结 ​

text
Node 和 Component 的成本来自引擎管理,不只是渲染。
合理拆分、可见项更新和对象复用比盲目减少节点更可靠。

十一、下一课预告 ​

text
Lesson062|Widget、Layout、ScrollView 的刷新与遍历成本