H5游戏性能优化整理(cocos-js 3.16)
近期在一家公司负责H5游戏加载速度优化,这里把近期做的项目优化项做一个整理分享:(若文中有错误的地方,还请指出。)
分享流程:了解html渲染流程 -> html相关优化 -> http相关优化 -> 项目结构和游戏流程及优化 -> 游戏渲染相关优化 -> 代码编写优化
html渲染流程
HTML解析过程:构建DOM树、构建CSSOM树、根据DOM树和CSSOM树构建render树、有了render树就开始布局Layout、最后绘制paint。
1、构建DOM树: 将HTML构建成一个DOM树,也就是构建节点,把所有的节点都构建出来。
2、构建CSSOM: 解析css去构建CSSOM树。
3、构建render树: DOM已经构建好了,css也有了,浏览器就会根据这两个来构造render树。
4、布局: 当render树有了,通过render树,浏览器开始计算各个节点的位置和样式。
5、绘制: 遍历render树,在页面上绘制每个节点。
6、重排reflow: 当render树绘制完成之后,比如JavaScript改变样式或添加节点,这时候render树就需要重新计算。
7、重绘repaint: 重新绘制页面。
HTML整个解析过程看起来很简单,但是我们要知道解析过程中css、js和dom的加载顺序。我们都知道HTML是自上往下解析的,在解析过程中:
1、如果遇到link和style,那就就会去下载这些外部的css资源,但是css跟DOM的构建是并行的,就是说不会阻塞DOM树的构建。
2、如果遇到script,那么页面就会停止html的解析和渲染把控制权交给JavaScript,直到脚本加载完毕或者是执行完毕。
1> 没有defer和async标签的script会立即加载并执行。
2> 有async标签的js,js的加载执行和html的解析和渲染并行。
3> 有defer标签的js,js的加载和html的解析和渲染并行,但会在html解析完成后执行,在触发DOMContentLoaded事件前执行。
3、页面的渲染是依靠render树,也就是说如果css没有加载完成,页面也不会渲染显示。
4、JavaScript执行过程中有可能需要改变样式,所以css加载也会阻塞JavaScript的加载。
5、JavaScript执行过程中如果操作DOM,但是DOM树又是在JavaScript之后才能构建,就会报错,找不到节点。
6、DOMContentLoaded和onload的区别:DOMContentLoaded在html解析完毕后执行,loload在页面完全加载完成后执行(包括样式和图片)。
html相关优化
其中对我们项目首屏启动速度影响最大的就是网络请求,所以优化的重点就是使用文件缓存和减少Http请求(页面中每发送一次请求,都会完成请求+响应这个完成的HTTP事务,会消耗性能,造成HTTP链接通道阻塞)。
1.html代码压缩。
2.减少页面上引用的文件数量(非首屏依赖CSS或者js文件、图片资源的引用,等首屏展示后静默下载(按实际项目需求))。
3.减少域名查询:DNS
查询和解析域名也需要消耗时间,不同域名使用越少越好。
4.优化页面加载顺序:1.将css
文件放在head
中 2.js
放在body
的底部。
5.防抖动、节流.
6.css相关优化
1.正确使用 Display 属性,因为 Display 属性会影响页面的渲染。
2.避免图片和 iFrame 等空 Src。
3.尽量避免重设图片大小。
4.避免 CSS 表达式。
5.移除空的 CSS 规则。
6.不滥用 Web 字体、Float。
7.不声明过多的 Font-Size。
8.值为 0 时不需要单位。
以下贴出示例的html文件内容,以注释进一步说明优化内容:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>title</title> <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1, minimum-scale=1,maximum-scale=1" /> <meta name="full-screen" content="yes" /> <meta name="x5-fullscreen" content="true" /> <meta name="360-fullscreen" content="true" /> <meta name="screen-orientation" content="portrait" /> <meta name="x5-orientation" content="portrait"> //初始化只加载首屏页面渲染依赖的css文件,非初始化依赖css文件,等游戏主包js下载后初始化由WebCssManager.js动态引入 <link rel="stylesheet" href="./css/layout.css"> </head> <body onload="load()"> <script src="src/res/loading.js"></script> //游戏定制页,在主包下载之前过度显示,避免因下载主包导致首屏展示太慢,提高游戏体验 <script> //SDK或者外部插件,全部由插件管理类管理,初始化依赖的SDK在管理类下载完后立即执行,其他插件或者渠道SDK等主包下载完成后下载 var loadPluginManager = function() { var script = document.createElement('script'); script.onload = function() {//下载js主包} script.onerror = function(err) {loadPluginManager();} script.src = "./plugin/webPluginManager.js"; document.body.appendChild(script); } loadPluginManager(); </script> <canvas id="gameCanvas" width="1136" height="640"></canvas></body> </html>
//html代码逻辑尽量少,只引入少量文件避免过多的HTTP请求
HTTP相关优化
1.如果支持http合并请求就合并请求。合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
静态资源打包,因为浏览器下载静态文件的时候是有线程数限制的:同一时间针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞。为了提高性能,服务器端会把js/css 合并成一个文件(因为都是文本嘛)再向客户端输出,这样页面能更快的展现。
2.缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存localStorage。
项目结构和游戏流程及优化
示例:打包出来的项目结构(不含子包),其中主包js文件大小2.9MB左右,资源文件大概15MB(压缩后,png类型资源在打包的时候会额外生成一份Webp格式(android使用webp、ios使用png))。
项目结构优化:
1.(棋牌大厅)游戏主包或者游戏子包(棋牌游戏)的发布直接覆盖原包内容(便于没变动文件使用缓存),其中主包目录和子包目录独立分开,便于版本缓存管理。
资源相关优化:
1.打包并压缩所有游戏js文件。
2.合并所有CSS文件(按自身项目情况而定)。
3.合并非初始化所需插件或者SDK的js文件(按自身项目情况而定)。
4.压缩图片资源,png纹理生成一份webp格式(并再次压缩75%)供android使用(按自身项目情况而定,因为webp纹理下载后需要解码,算上解码耗时小图使用webp不划算)。
5.合并所有资源集合图plist文件(减少大量http请求数、IO)。
6.项目结构中大厅游戏根据版本号修改html文件和主包文件名,资源文件使用文件MD5命名,游戏子包根据服务器下发版本号区分http请求。
流程逻辑相关优化:
前言:
因为我们游戏APP、微信小程序、h5都是共同游戏服务器,针对h5通讯流程后端没做任何优化,共用一套(连接大厅服务器(含请求进入大厅相关接口)、连接游戏服务器(webSocket)(含请求进入游戏房间相关接口)耗时长),充分利用通讯耗时做该做的事情(如下载图片资源):异步执行游戏中同步顺序执行的逻辑(按实际情况而定)。
游戏优化前流程(只列举主要流程):
1.正常方式登录大厅:预览登录资源 -> 连接大厅服务器 -> 请求进入大厅协议 -> 预览大厅主界面资源及通用资源 -> 显示大厅
2.通过分享连接进游戏:预览登录资源 -> 连接大厅服务器 -> 请求进入大厅协议 -> 预览大厅主界面资源及通用资源 -> 显示大厅 -> 连接游戏服务器 -> 请求进入房间 -> 预览游戏资源 -> 显示游戏房间界面
游戏优化后流程(按实际项目情况而定,主要保持异步耗时平衡):
1.正常方式登录大厅:
(连接大厅服务器 -> 请求进入大厅协议) 同时异步执行 预览登录和大厅主界面及通用资源 -> 显示大厅
2.通过分享连接进游戏:
(连接大厅服务器 -> 请求进入大厅协议) 同时异步执行 预览登录和通用资源 -> (连接游戏服务器 -> 请求进入房间) 同时异步执行 预览游戏资源 -> 显示游戏房间界面
详细游戏优化相关说明:
1.连接大厅服务器、请求大厅相关接口的同时下载游戏资源(部分资源直接载入纹理缓存)(此处必须保持异步平衡,连接大厅耗时充分用来下载和预览资源)(注意控制同时请求数量,因为大部分浏览器并发请求数是4-6个)。
2.进游戏房间同上。
3.异步执行影响游戏流程中部分独立不互相影响的流程(如GPS、影响游戏流程的接口)。
4.通过分享进游戏,只预览少量大厅通用资源和对应游戏资源,跳过大厅相关流程且不创建大厅相关界面,直接进游戏房间(通讯和游戏资源下载预览同上,且保持异步平衡)。
游戏渲染优化
1.相同图集里的图一次性连续渲染完,减少Draw Call。
2.减少被遮挡或者超出可视区的单位渲染。
3.部分UI元素较多的界面或者场景建议使用分帧加载,元素量大的则规划渲染规则。
4.允许情况下减低帧率。
CPU
引发的问题:
- 由于短时间内的计算量太大,导致画面流畅性降低,俗称跳帧
- 发热严重,耗电量高
常见的优化手段:
- 将计算分到多个逻辑帧中进行计算,避免短时间内的性能超过负荷,俗称“分帧”(time-slice)。
- 将可以缓存的数据尽可能的缓存起来,避免重复计算和重复分配内存,常见的示例为“内存池”。
- 使用合理的算法和数据结构,比如:冒泡排序和直接插入排序在整体数组比较有序的情况下效率大大好于快速排序。把快排替换成是优化程序排序效率的一个常见的思路。
GPU
引发的问题:
- 发热严重,耗电量高
- FPS降低
常见的优化手段:
- 优化美术资源,比如合理规划图集,约定好模型的最大三角形面数,制定合理的粒子效果规范。这个可以说是游戏优化中最重要的一个,因此,技术美术在游戏开发中作用巨大。
- 简化或者优化着色器(shader),如在游戏开始前就对Shader进行编译和加载。
- 使用Batching,尽量减少DrawCall
- 使用平台推荐的压缩格式,比如安卓平台的ETC1和IOS平台的PVRTC
IO和网络
引发的问题:
- 网络延迟甚至掉线
- 加载资源导致的跳帧
- 加载时间过长
常见的优化手段:
- 使用独立的线程进行加载,有些引擎如Unity中还能利用协程
- 减少网络包里面的冗余数据
- 合并小包,减少请求数据的次数
- 分帧对回包进行处理
- 限制一定时间内的发包频率
内存
引发的问题:
- 闪退和卡死,比如安卓的Low Memory Killer会在低内存情况下杀掉内存占用过大的程序。
常见的优化手段
- 动态加载和卸载资源,比如在游戏内的时候,我们可以把游戏外的一些UI图集卸载掉。
- 降低资源质量或屏幕分辨率,这是有损优化,一般作为最后的手段
代码编写优化
这里先推荐一本书:《代码整洁之道》:代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。
js原生库推荐 : Lodash (一个一致性、模块化、高性能的 JavaScript 实用工具库。它内部封装了诸多对字符串、数组、对象等常见数据类型的处理函数)。
1.适当使用函数式编程。
2.尽量使用异步编程。
3.在js中尽量减少闭包的使用。
4.封装尽量做到低耦合高内聚。
5.整理代码规范,避免破窗效应。
6.设计模块化、组件化、分层化。
7.避免使用全局数据。
8.避免编写相似得函数。
9.养成不断的批判对待自己代码的习惯,寻找重新组织、改善结构和正交性机会。