2024 抖音欢笑中国年(六):前端互动场景中的性能优化


theme: nico

作者:陆泽耿、陈河兵、陈钰鸣、王武俊

前言

在端内前端互动场景中,由于需要加载互动引擎,以及场景文件、模型文件和纹理贴图等资源,页面资源请求数量比传统的前端页面多了很多,对页面的FMP(First Meaningful Paint) 会有较大的影响。

同时前端页面与宿主共享有限的进程资源,如果没有控制好内存水位,CPU 和 GPU 占用率等性能指标,可能会导致客户端发烫,页面卡顿和甚至闪退

为了保障互动页面的快速启动和运行时的稳定性,我们提供了一系列的性能优化策略对资源加载和运行时的性能进行优化,下面将结合2024 年抖音欢笑中国年的部分项目介绍相关的性能优化策略。

启动速度优化

互动场景下,影响首屏性能的因素主要有两个方面:引擎运行和加载游戏资源。为了降低引擎初始化的耗时,我们前期做了一些工作,比如将引擎拆成颗粒度更小的包、移除全局自执行逻辑、优化渲染管线等。而在这次春节活动中我们将引擎内置到互动容器,在容器实例化之后加载业务页面之前创建引擎实例,从而可以更早的运行引擎初始化逻辑。引擎预热具体细节可以参考往期文章2024 抖音欢笑中国年(二):AnnieX互动容器创新玩法解析,接下来重点介绍下游戏资源加载的优化。

游戏资源相对于前端页面资源存在的特点是,类型多、数量多、文件大小不等、资源间存在依赖关系等,针对这些问题,做了如下优化:

预请求

如果要更快的拿到资源请求的结果,需要尽早的发送出请求:通过配置 manifest 文件,在容器启动后优先按序发出资源的请求。但是容器对资源的 prefetch 依然存在资源数量和资源大小的限制,prefetch 的资源数量太多,依然会造成网络资源的竞争;资源太大,新请求回来的资源会刷新缓存资源的物理内存池,导致无法内存共享。

在互动场景中,除了游戏资源,还有UI的资源,如果要首屏直出,UI 资源是需要保障优先发出和加载的。我们对 UI 资源和游戏资源做了细分,在 manifest 文件中只配置首屏必须的资源且尽可能保证资源体积大小在容器的限制范围内。

资源拆分及合并

互动场景会广泛使用 AssetBundle 方案来对资源做序列化,从而达到更好的组织编排资源。在存在 3D 模型资源的情况下,资源数量会多很多,其中包含 Geometry、Material、Texture、Image等,有些资源会很小,只有几K,有些资源会很大,可以到几M。大的文件和小的文件在移动网络环境下传输都没法充分利用网络的并发能力来更快的获取资源,所以我们在序列化过程中对资源重新做了编排。设置文件的上限 300K,将小文件合并到一起更接近 300K,将大文件拆分成多个 300K 的文件。

拆分合并结果对比:

玩法 神龙寻宝 神龙探宝 守卫现金 摇福签
优化前文件数量 180+ 25 12 20
优化后文件数量 40 19 8 4

除了上述优化之外,还有抖音通用的静态资源分发平台下发、跨端框架渲染队列优化等,都为首屏加载提速奉献了力量。通过性能埋点,整体上较去年春节活动项目在 FMP 上面有了大幅的提升。

FMP pct90( ms ) 抖音极速版
Android 今年 2,232.79
对比去年 -25.49%
iOS 今年 994.73
对比去年 -4.95%

运行时性能优化

为了更好地理解运行时优化思路,在介绍具体优化策略之前,先简单介绍一下互动引擎的核心渲染模块工作原理。

如上图所示,互动引擎核心渲染模块在每个帧循环中的工作主要分为三个阶段:

  • CPU 阶段

    • CPU 阶段的工作主要是进行渲染数据的准备,包括动画数据计算、材质更新等
  • CPU -> GPU 阶段

    • CPU -> GPU 阶段的工作主要是渲染数据的上传和具体绘制指令的发起,这些工作最终会封装成图形API的调用,其中主要的内存开销来自于模型数据和纹理贴图
  • GPU 阶段

    • GPU 阶段的工作主要是图形绘制和渲染结果上屏

从引擎核心渲染模块的实现我们可以知道,运行时性能优化的工作需要围绕减少这三个阶段的计算量和内存占用来开展,下面将具体介绍我们在抖音欢笑中国年相关项目中所使用到的优化策略。

CPU / GPU 优化

动态FPS(根据机型、评分动态调整FPS)

对于一般的游戏,我们使用 30 FPS 的帧率来保证用户的流畅体验,“招财神龙”玩法这次综合性能和用户体验,对高端机采用了 60-120FPS 的设置,而对性能较差的低端机,依旧保持30FPS的帧率。

GAME_MAX_FPS: {
  enable: false, // 全局开关,如果设置为false,则不启用
  useEverySecond: true, // 是否在这个基础上跳帧
  i32Forbidden: true, // 32为包禁用这个动态FPS的能力
  blackList: [], // 机型黑名单
  deviceScoreHigh: 10, // 超过这个评分算高端
  deviceScoreMid: 8, // 超过这个评分算中端
  deviceLevel: ['high'], // 启用的机型
  frameSkipThreshold: 90 // 当真实FPS超过多少时,若未开启跳帧,则开启跳帧
},

首先,我们会统计3s内的用户手机真实运行 FPS

如果根据动态配置,我们可以开启动态FPS并且用户手机的真实 FPS > 30帧率的话,我们会将用户的手机的FPS设置为最大的FPS, 如下图,如果 maxFPS = 0,即代表用户引擎对渲染不限帧

但是对于个别机型,可能会有 120 / 144 的刷新率,导致计算太频繁,于是,我们在以上基础上提供了一个frameSkipThreshold属性, 当用户真实FPS超过这个阈值的时候,采取跳帧策略,保障性能稳定性

批次合并

一个较为精细 3D 模型通常是由成千上万的三角面片组成的,而 2D 元素则相对简单很多,例如一个普通模式的Sprite 的几何数据仅为两个三角面片,但是当场景中 2D 元素的数量较多时也可能会引起性能问题,这些 2D 元素可能带来频繁的渲染上下文切换和 drawcall 指令的发起,从而增加 CPU 和 GPU 的负担。

常见的优化策略为批次合并,通过将渲染条件相同的 2D 元素的几何数据合并在一起,最终使用一次 drawcall 绘制多个 2D 元素。

下图是“招财神龙”玩法的其中一个「寻宝场景」,可以看得出来,这个场景本身节点数量较多,层级关系较为复杂,如果不进行优化,可能导致运行时的卡端。

由于场景中的节点使用的都是 Sprite 渲染组件,我们只需要保证 Sprite 组件所引用的纹理都是相同的就能触发引擎的自动合批功能,在运行时对 Sprite 节点进行批次合并渲染。

如下图所示,我们将场景中所用到的图片通过 TexturePacker 合并到了一张大的纹理上,以此保证 Sprite 组件引用的纹理相同。

最终运行时的效果如下图所示,左边是没有开启自动合批的情况下,drawcall 为34。右边是开启了自动合批的功能,相同场景下 drawcall 下降到14,降低了近 60% 。

Spine 优化

“保卫现金” 玩法中,我们使用了 Spine 作为动效方案。然而,在性能测试中,我们发现在低端机上会出现发热和卡顿的情况。因此,我们需要对 Spine 进行优化和调整,以确保性能的稳定。

一、减少骨骼数

在 Spine 动画中,骨骼数会对性能产生较大的影响。具体来说,有以下几个方面:

  1. 每个骨骼进行变换时,都需要 CPU 来计算其位移、旋转和缩放等属性。更多的骨骼意味着更多的计算,这可能会在低功率设备上导致 CPU 瓶颈,从而影响帧率和响应速度。
  2. GPU 需要根据骨骼的变换信息来处理顶点数据,实现最终的动画效果。如果顶点绑定到的骨骼数目多,GPU 的工作量就会增大,这可能会降低渲染效率,特别是在顶点数量和骨骼数量都很高的情况下。
  3. 处理更多骨骼导致的性能负担可能会使设备生成更多的热量,特别是在长时间运行高负载应用时。

在“保卫现金”玩法中,场景中的 Spine 会自动循环高频播放,高骨骼数和高动画复杂度会对性能产生较大影响,尤其是在性能较差的低端机上。因此,针对这个情况,我们需要对 Spine 动画资源进行降低骨骼数的优化,以达到降级动画复杂度的目的。

二、禁用裁剪附件

为了实现玩法中“打地鼠”的效果,我们最初的想法是通过设计师使用 Spine 的裁剪附件来实现年兽的出现消失的遮挡效果。然而,在性能测试中,我们发现使用 Spine 的裁剪附件会导致渲染压力非常大。因此,我们需要禁用 Spine 的裁剪附件,改为通过 SarMask 组件的透明度遮罩来实现想要的效果。

简单来说,即使我们将 Spine 动画的 Cache Mode 设置为了 ShaderCache,但是对于使用了 Clip 节点的 Spine 动画,仍然会在做实时剔除不在 Clip 节点范围内的三角形。加上这些 Spine 动画自身的骨骼数量都较多(加上三角形过多),所以导致了 CPU 侧实时运算过多,从而出现卡顿的情况。

改为Sar Mask组件实现效果如下,通过遮罩来实现对年兽的遮挡效果

三、单面渲染
const spineComponent = spine.getComponent(Spine);
spineComponent.materialSide = Sides.Front;

我们可以手动做材质排序,将 Spine 的渲染分组(render group)设置成相同的,并将 Spine 改为单面渲染,以减少 program 切换次数,降低 CPU 占用。

内存优化

Profile 工具

从上面的引擎核心渲染模块介绍我们可以知道,运行时主要的内存开销来自于模型几何数据和纹理贴图。如果模型或者纹理的尺寸过大,会导致运行时过多的内存占用,有可能会导致设备 OOM 。

为了方便开发同学对运行时内存占用进行调试,我们在编辑器提供了内存 Profile 工具。

具体操作方式如下面的视频所示,通过 Profile 工具可以抓取到页面运行过程中所创建的几何对象和纹理对象,抓取列表中会显示不同资产所占的内存大小。

开发同学可以在开发过程中使用 Profile 工具及时发现不符合规范的资产,与设计同学沟通修改,保障最终在运行时不会出现内存占用过大的问题。

暂时无法在飞书文档外展示此内容

纹理资产优化

针对纹理资产优化,我们在引擎中支持了压缩纹理和纹理降级

  • 压缩纹理

    • 压缩纹理是一种专门为图形渲染设计的图像存储压缩技术,支持在 GPU 中进行随机的纹理块访问而不需要解压整张纹理,从而减少运行时的内存占用,常见的纹理压缩格式有 ETC,ASTC 等。
  • 纹理降级

    • 纹理降级指的是在不影响业务代码逻辑和渲染元素尺寸的前提下,将性能较低的设备上运行的页面引用的纹理贴图替换成较低分辨率的技术手段。

这两种优化策略都集成到了编辑器的工作流中,开发同学只需要在编辑器中进行相应的配置,就能开启纹理压缩和降级的功能,具体的操作方式参考2024 抖音欢笑中国年(三):编辑器技巧与实践相应章节的介绍。

如下图所示,对于大多数不需要在运行时动态修改的纹理,可以在编辑器中关闭纹理的 Enable write,引擎内部会在纹理上传到 GPU 之后自动释放 CPU 的纹理对象,从而减少内存占用。如果是界面 UI 所使用的纹理,可以同时关闭 Generate Mipmaps,进一步减少内存占用。

未来展望

互动引擎已经连续两年支持了春节活动的开发,在抖音、抖音极速版、今日头条和西瓜视频等公司内多个APP上均有业务落地。而随着前端互动业务的发展,更多的页面元素,更加丰富的模型细节,更加复杂的渲染效果,都对引擎的稳定性和流畅性带来挑战。

于是我们计划推出互动引擎2.0,进一步释放移动端的性能潜力。相对于引擎1.x,2.0进一步结合跨端框架,对引擎核心渲染模块进行了重构并下沉到 Native 实现,底层使用 ECS 架构双线程的运行时架构,为业务提供更小的包体更快的启动速度更强的运行时性能

针对计算开销较大的第三方模块,比如 Spine 特效,我们也将使用 C++ 版本的实现,通过编译成 Wasm 模块来提升运行时性能,并提供多种缓存策略来减少运行时计算开销。

互动引擎的2.0将在2024年中推出第一个版本,预期会支持2025年春节活动的开发,敬请期待。

团队介绍

我们是抖音前端架构-互动体验技术团队,主要为字节跳动业务提供互动技术解决方案。技术产品包含面向互动 / 小游戏研发场景的 SAR Creator、高性能动效渲染引擎 Simple Engine、互动场景端能力套件 AnnieX 互动容器

在这些技术建设与业务落地上,和抖音前端-互动创作团队跨端框架团队、开放平台小游戏团队、用户增长-激励前端团队一同推进,不断探索字节跳动应用生态下的创新业务形态。

往期回顾

2024 抖音欢笑中国年(一):招财神龙互动技术揭秘

2024 抖音欢笑中国年(二):AnnieX互动容器创新玩法解析

2024 抖音欢笑中国年(三):编辑器技巧与实践

2024 抖音欢笑中国年(四):渲染技术实践与探索

2024 抖音欢笑中国年(五):Wasm、WebGL 在互动技术中的创新应用


这是一个从 https://juejin.cn/post/7372115662464581683 下的原始话题分离的讨论话题