HumanBreak/docs/guide/ui-perf.md
2025-03-13 19:38:39 +08:00

6.9 KiB
Raw Blame History

lang
zh-CN

UI 优化指南

多数情况下,我们编写的简单 UI 并不需要特别的性能优化,渲染系统的懒更新及缓存机制已经可以有很优秀的性能表现,不过我们还是可能会遇到一些需要特殊优化的场景,本节将会讲述如何优化 UI 的性能表现,优化建议包括:避免元素平铺;使用 ScrollPage 组件优化平铺性能;避免元素自我更新;使用 cachenocache 标识;特殊场景禁用抗锯齿和高清;在合适场景下隐藏一些元素等。

避免元素平铺

在不使用 Scroll 组件时,我们需要尽量避免元素平铺,因为这会导致更新时渲染次数上升,从而引起性能下降。我们建议善用树形结构的缓存特性,将可以作为一个整体的元素使用一个容器 container 包裹起来,减少更新时的渲染次数,尤其是对于那些不常更新的元素来说,更应该使用容器包裹。不过我们也不建议嵌套过深,这可能导致浪费在递归渲染上的时间过长,渲染效率变低。

画布渲染树的深度遍历特性使得:

  • 每个独立容器的更新会触发子树的重新渲染
  • 容器层级过深会增加递归调用栈开销
  • 合理分组可将高频/低频更新元素隔离

下面是代码示例:

// ❌ 差的写法,全部平铺在一个容器里
<container>
    <text text="1" />
    {/* 中间省略 998 个元素 */}
    <text text="1000" />
</container>

// ✅ 好的写法
<container>
    {/* 把不常更新的单独放到一个容器里面 */}
    <container>
        <text text="1" />
        {/* 中间省略 988 个元素 */}
        <text text="990" />
    </container>
    {/* 把常更新的单独放到一个容器里面 */}
    <container>
        <text text="991" />
        {/* 中间省略 8 个元素 */}
        <text text="1000" />
    </container>
</container>

使用 ScrollPage 组件优化平铺性能

在一些特殊情况下,我们不得不使用平铺布局,例如上一节提到的怪物手册,或是展示一个列表等,这时候必须平铺元素。这时候我们可以使用 Scroll 组件或 Page 组件来优化性能表现。Scroll 组件中,只有在画面内的元素会被渲染,而画面外的不会被渲染,这会大大提高渲染效率;Page 组件允许你把列表拆分成多个部分,然后把内容放在不同页中,从而提高渲染性能。极端情况下,Page 组件的渲染效率要明显高于 Scroll 组件,但是滚动条对于交互更友好,我们推荐在简单场景下使用 Scroll 组件,而对于复杂场景,换为 Page 组件。两个组件的使用方式可以参考 API 文档

我们建议:

  1. 优先使用 Scroll
    • 元素数量 < 500
    • 需要流畅滚动交互
    • 元素高度不固定
  2. 切换至 Page
    • 元素数量 > 1000
    • 需要支持快速跳转
    • 存在复杂子组件(如嵌套动画)

下面是代码示例:

// ❌ 差的写法,全部平铺在一个容器里
<container>
    <text text="1" />
    {/* 中间省略 998 个元素 */}
    <text text="1000" />
</container>

// ✅ 好的写法,使用 Scroll 组件优化
<Scroll>
    <text text="1" />
    {/* 中间省略 998 个元素 */}
    <text text="1000" />
</Scroll>

// ✅ 好的写法,使用 Page 组件优化
<Page>
    {(page: number) => {
        return list.slice(page * 10, (page + 1) * 10).map(v => <text text={v.toString()} />)
    }}
</Page>

避免元素自我更新

元素自我更新是指,在元素的渲染函数内,触发了元素的冒泡更新,这会导致更新无限循环,而且难以察觉。为了解决难以察觉的问题,我们使用了一种方式来专门探测这种情况。常见的触发元素自我更新的场景就是使用 sprite 元素,例如:

const element = ref<Sprite>();
const render = () => {
    element.value?.update();
};

<sprite render={render} ref={element} />;

在上面这段渲染代码中,sprite 元素的渲染函数又再次触发了自我更新,这会导致更新无限循环。在开发环境下,这种情况会在控制台抛出警告:Unexpected recursive call of Sprite.update?uid in render function. Please ensure you have to do this, if you do, ignore this warn.,这会告诉你是哪个类型的元素触发了循环更新,以及对应元素的 uid,从而帮助你寻找问题所在。不过,样板还是留出了一个口子,如果你必须使用循环更新,那么你可以忽略此条警告,在网站上游玩时这条警告将不会被触发,游戏会正常运行。

使用 cachenocache 标识

cachenocache 表示可以让你更加精确地控制渲染树的缓存行为,从而更好地优化渲染性能。默认情况下,这些元素是会被缓存的:container container-custom template sprite image icon layer layer-group animation,对于这些元素,你可以使用 nocache 标识来禁用它们的缓存,对于其本身或其子元素的渲染较为简单的场景,禁用缓存后渲染效率可能会更高。其他元素默认是禁用缓存的,如果你的渲染内容比较复杂,例如 g-path 元素的路径很复杂,可以使用 cache 表示来启用缓存,从而提高渲染效率。示例代码如下:

const render = (canvas: MotaOffscreenCanvas2D) => {
    canvas.ctx.fillRect(0, 0, 200, 200);
};
// ❌ 差的写法,一个简单的矩形绘制,但是 sprite 默认启用缓存,可能会拉低渲染效率
<sprite render={render} />;

// ✅ 好的写法,使用 nocache 标识禁用 sprite 的缓存机制
<sprite render={render} nocache />;

特殊场景禁用抗锯齿和高清

默认情况下,大部分元素都是默认启用高清即抗锯齿的(layerlayer-group icon 不启用),这可能会导致一些不必要的计算出现,从而拉低渲染性能。对于一些需要保持像素风的内容,我们建议关闭抗锯齿和高清画布。代码示例如下:

// ❌ 差的写法,像素风图片使用默认设置,启用了抗锯齿和高清
<image image="pixel.png" />
// ✅ 好的写法,关闭了默认的抗锯齿和高清
<image image="pixel.png" noanti hd={false} />

在合适场景下隐藏一些元素

如果一个元素在某些场景下需要隐藏,另一些场景下需要显示,我们建议使用 hidden 属性来设置,而不是通过把它移动到画面外、调成透明颜色、使用 if 或三元表达式判断等方式。示例代码如下:

// ❌ 差的写法,使用条件表达式切换元素显示与否
{
    !hidden.value && <sprite />;
}
// ✅ 好的写法,使用 hidden 属性
<sprite hidden={hidden.value} />;

后续计划

我们后续计划推出渲染树调试工具,届时可以更加细致方便地查看渲染树的渲染情况以及性能问题。