微信小游戏 - 小游戏 vs H5 游戏性能对比和分析

这是个人关于微信小游戏系列文章的第三篇,在这系列文章里会描述 ——

  • 如何把一些 Canvas/WebGL Demo 移植到小游戏环境并支持双端运行;
  • 对小游戏在 Android 平台的运行时架构进行分析;
  • 通过对移植的 Canvas/WebGL Demo 在小游戏和 Chrome for Android 浏览器上做 Benchmarking,对 H5 游戏 vs 小游戏的渲染性能进行对比和分析;

测试说明

测试软件

  1. 微信 6.6.3
  2. Chrome for Android 64.0.3282.137

测试使用的 Demo 可以通过 GitHub 下载。

测试设备

小米 Note3,使用骁龙 660,算是这两年比较主流的中端芯片,虽然性能在高负荷场景下跟旗舰芯片如骁龙 835 还有一定距离,不过大部分场景下也已经够用。

测试环境

设备电量在 60% 以上,处于充足状态,并使用风扇进行辅助散热,避免因为电量不足或者过热导致设备锁核降频,从而引起测试数据的波动,无法获得可以对比的测试结果。

页面在 Chrome 上运行在全屏的 WebApp 模式下,如果没有额外说明,Canvas 的渲染分辨率使用设备的屏幕分辨率 1080p。

帧率获取

adb shell "dumpsys SurfaceFlinger --latency 'TARGET_WINDOW_NAME'"

使用上述命令可以获取目标窗口最近 127 帧的更新时间戳,然后用脚本计算出这 127 帧渲染过程中的平均帧率。

另外代码中也通过计算 JavaScript 中每帧调用的次数和时间间隔来计算帧率,然后通过控制台打印输出类似下面的信息,实际验证两者的结果基本一致。

WebGL Aqua framerate:1.55fps, program:44, draw call:118

本文的测试数据使用第一种方法

测试数据

WebGL Compute

模拟鸟群的运动,包含大量的物理运动计算,实际上是测试 JavaScript 的计算性能。

可以对 numBoids (guimark3.js)参数进行修改,改变鸟群的数量,鸟群数量越大,JavaScript 计算的耗时就越大。

运行环境 250 鸟群 500 鸟群
小游戏 58 ~ 59 21 ~ 22
Chrome 58 ~ 60 20 ~ 21

WebGL Aqua

绘制的场景有一定的复杂度,包含了约 30 个模型,使用了 44 个 Program,当参数设定为 500 条鱼时,需要调用 600 多个 Draw Call(不使用 Instance Rendering 的情况下)。

可以配置的参数如下:

  1. fishSetting 可以修改鱼的数量(aquarium-common.js);
  2. antialias 可以选择开启或者关闭抗锯齿(webgl.js);
  3. TRY_USE_WEBGL2 可以选择是否使用 Instance Rendering(wxhelper.js);
  4. 使用 GetCanvasSizeUseWindowRatio(720/600) 或者 GetWindowSizeInPx 选择不同的渲染分辨率(aquarium.js - setCanvasSize);
运行环境 100 条鱼 200 条鱼 500 条鱼 1000 条鱼 2000 条鱼
小游戏 52 ~ 53 52 ~ 53 58 ~ 59 58 ~ 59 37 ~ 38
Chrome (关闭抗锯齿) 60 60 60 58 ~ 60 39 ~ 40
Chrome (关闭抗锯齿, 720p) - - - - 60
Chrome (关闭抗锯齿, Instance) - - - - 52 ~ 54
Chrome (打开抗锯齿) 60 60 42 ~ 44 28 ~ 30 18 ~ 19
Chrome (打开抗锯齿, 720p) - - 60 51 ~ 53 31 ~ 32
Chrome (打开抗锯齿, 600p) - - - 60 42 ~ 43
Chrome (打开抗锯齿, Instance) - - 56 ~ 60 47 ~ 51 35 ~ 36
Chrome (打开抗锯齿, Instance, 720p) - - - - 58 ~ 60
  1. 小游戏不支持抗锯齿,无论设置 antialias 参数为 true 或者 false,结果都是 false,Chrome 默认是打开抗锯齿,需要显式关闭;
  2. 小游戏目前并不支持 WebGL2,WebGL2 新增的 API 并没有实现;
  3. 小游戏只能使用屏幕像素大小的渲染分辨率;
  4. 小游戏在 100 ~ 200 条鱼时帧率反而更低,反复验证过几次均是如此,怀疑踩到了什么坑,应该不是性能瓶颈;
  5. 小游戏最高帧率不会达到 60,最多就是 58 ~ 59,或许是 requestAnimationFrame 的实现有些问题;

Canvas Bitmap

类似雷电的小游戏,多个小位图的重复绘制,主要测试 Canvas.drawImage 的性能,跟微信开发工具自带的样例游戏类似。

可以对 enemiesCount(guimark3.js),改变敌机的数量,从而增加或者减少 drawImage 的调用次数。

运行环境 ~1000 drawImage ~2000 ~4000
小游戏 58 ~ 59 58 ~ 59 51 ~ 53
Chrome 60 60 30 ~ 31

性能分析

渲染流水线

在 Chrome 渲染流水线里面,Canvas 元素的更新跟其他 DOM 元素的内容更新一样,都需要走非合成器动画的渲染流水线。而非合成器动画渲染流水线过于复杂和冗长,比较小游戏简单和直接的渲染流水线,会有较多的 Overhead。

不过假设网页的运行条件跟小游戏一样,页面只有一个 Canvas 元素而不存在其他 DOM 元素,这些 Overhead 其实每个环节的耗时都很小,加上 Chrome 多线程高并发的流水线设计,实际上大部分开销都可以忽略不计,对整体性能的影响微乎其微。

WebGL Compute 的测试结果也验证了这一点,该 Demo 只有一个 Draw Call,所以我们基本可以忽略绘制部分的影响,JavaScript 计算的开销也可以认为是基本相同,在小游戏和 Chrome 的性能测试结果基本一致的情况下,我们可以推断出渲染流水线本身不会造成明显的性能差异。

关于 Chrome 非合成器动画的渲染流水线可以参考我的文章 - 浏览器渲染流水线解析与网页动画性能优化。

JavaScript Computing

我们把 JavaScript Computing 定义为除了 Native API(2D Canvas,WebGL,etc…)外的其它纯 JavaScript 代码运行的耗时。

小游戏和 Chrome 都使用 v8 虚拟机运行 JavaScript 代码,理论上 JavaScript 计算的性能不会存在较大差异,些微的差别可能来源自 v8 的版本差异,WebGL Compute 的测试结果也验证了这一点。

WebGL

Chrome 对比小游戏 WebGL 绘制的差别在于:

  1. 小游戏的 WebGL 调用会直接调用对应的 GL API,Chrome 使用了跨进程/线程的 CommandBuffer 机制,WebGL 调用会先被 Encode Command 到 Buffer 里面,然后在另外一个独立的 GPU 线程中 Decode Command,再调用相应的 GL API;
  2. 小游戏的主 Canvas 直接绘制在 GLSurfaceView 上,Chrome 会为每个 Canvas 元素分配额外的 Texture 作为 Render Target,然后再把该 Texture 合成到 Window 上,这意味着 Chrome 需要做一次 Render Target 的切换和多一次缓存拷贝,会增加一些 GPU 开销;

在 WebGL Aqua 这个 Demo 中,计算部分的开销很小,大部分是绘制的开销,Chrome 的多线程模型并没有带来多少并发的优势,只是抵消了 CommandBuffer 机制带来的一些 Overhead,不过额外的 Render Target 切换和缓存拷贝的影响看起来也很小,Chrome 和小游戏的性能基本持平,甚至 Chrome 还稍微好一些。如果是计算和绘制开销比较平均的场景,Chrome 可能会有更大的性能优势。

2D Canvas

Chrome 对比小游戏 2D Canvas 绘制的差别在于:

  1. Chrome 每一个 2D Canvas Draw Call,特别是 drawImage 的调用,因为浏览器运行环境复杂性的原因,有较大的 Overhead,包括 Image 对象的类型检查和转型,Dirty Rect 的计算,DOM 对象的交互,Image URL 的 Origin Check 安全检查,等等,而这些在小游戏运行环境下都是可以省略或者优化的;
  2. Chrome 使用 Skia 作为 2D 绘图引擎,Skia 作为一个通用,完备,可外部配置的 2D 绘图引擎,支持不同的光栅化后端实现(CPU,GL,Vulkan),支持复杂的内存管理和资源缓存机制,但是也带来了较高的 per drawImage 的开销,小游戏高度定制的 2D 绘图引擎可以容易地规避掉这些开销;

因为 Chrome 运行环境中较高的 per drawImage 的开销,我们可以在 Canvas Bitmap 测试中看到当 drawImage 的调用次数非常高时,Chrome 的 Renderer 线程会被严重阻塞而导致帧率下降幅度大大高于小游戏的下降幅度。

WebGL 理论上 Chrome 也会多一些 Overhead,只是 WebGL API 相比 2D Canvas API 的实现要简单很多,所以这些 Oveahead 实际影响非常小,并没有 2D Canvas 那么明显。

总结和优化建议

总结

  1. Chrome 和小游戏因为渲染流水线不同带来的影响很小,基本上可以忽略(在没有其他 DOM 元素的条件下);
  2. Chrome 和小游戏都使用 v8 作为 JavaScript 虚拟机,JavaScript 的计算性能基本一致;
  3. Chrome 和小游戏的 WebGL 渲染性能在都关闭抗锯齿的条件下基本一致,Chrome 还略微超出,另外 Chrome 可以通过降低渲染分辨率和使用 Instance Rendering 等方式进一步提升性能;
  4. Chrome 和小游戏在 2D Canvas 绘制场景中,如果 drawImage 的每帧调用次数在正常范围内,性能差别不大,如果 drawImage 调用次数非常高,Chrome 的性能下降幅度更大;

H5 游戏的优化建议

  1. 如果渲染场景比较复杂,WebGL 建议关闭抗锯齿,打开/关闭抗锯齿在 5.x 寸屏上对渲染效果的影响微乎其微,并且小游戏和原生手游也没有使用抗锯齿,关闭抗锯齿对于复杂的渲染场景可以带来非常明显的性能提升;
  2. 如果渲染场景非常复杂,可以考虑使用较低的渲染分辨率,降低分辨率可以带来明显的性能提升,600 ~ 720p 在 5.x 寸屏上比起原生分辨率虽然画面会略微模糊一些,但是整体渲染效果还是可以接受的;
  3. 如果渲染场景存在同一个 Mesh 的大量重复绘制,建议使用 Instance Rendering,Instance Rendering 可以大幅降低 GL API 调用次数,减少 GL API 调用的 CPU 开销,带来较大程度的性能提升;
  4. 如果 2D 游戏场景中的每帧 Image 绘制次数达到几千的量级,建议使用 WebGL 替代 2D Canvas(大部分 2D 游戏应该也就几百的量级,就不需要考虑这个);

如果觉得本文的内容对你有帮助,请麻烦点下赞

你可能感兴趣的:(小游戏)