Cocos开发的Web小游戏加载优化

4,643 阅读8分钟

常见优化

其实无论是Web游戏或者其他普通网页,怎么样让用户能够更快的看到页面,更快的可以操作体验,都是一个持续讨论的话题。个人认为优化的方向主要分为两块:

  • 技术方向:比如缓存(上CDN)、压缩资源来减少传输体积等,基本与具体业务关系不大

  • 业务方向:就是根据具体项目定制优化了,不过其实对于某个具体业务,可能还是可以进一步细分技术相同技术的一系列业务,比如本文要提到的Cocos)和业务两个方向

抛开那些非常通用的优化手段,对于使用Cocos Creator开发的Web小游戏,常见的技术优化手段差不多就是

  • 剔除不需要的引擎模块,有几个大头如果不使用,完全可以剔除,比如3D模块、物理引擎相关、骨骼动画相关
  • 打包的时候,可以把主包压缩类型设置为合并所有JSON,毕竟Cocos打包生成的碎文件太多了,一下子那么多请求,阻塞一下,速度就会慢一点

上面两种都是Cocos Creator自带的能力,听起来已经很不错了,那还有什么问题需要优化?

问题

我们先来看一个Cocos Creatorv2.4.6版本打包一个空项目(_带一张图,一个ts文件做测试,项目地址:Cocos-Web,看看其加载过程:

1636466466799.jpg

从上图可以直观的看出,经过十几个请求之后,才开始加载主场景。而这个过程中,有诸多接口都是阻塞的,这也导致了在Fast 3G情况下,主场景要5.8s左右才开始加载,所有资源加载完需要8.1s左右!这天花板也太低了啊!

这么长的流程,用户早跑完了,我们来逐个分析一下这些请求都是干什么的吧。

Inline

一开始,当然是加载了一个Html,然后再加载了一个对应的CSS,这里是简单实现了一个加载页面。毕竟Cocos Creator的引擎文件还是很大的(可以看上面图片的cocos2d-js-min.xxx.js,gzip之后,还有260KB),没法一下子就下载完,故先在Html中实现了一个加载界面,不让用户等半天还是白屏。

对于这个加载界面,实际开发肯定是会替换掉的,而且这里的CSS体积并不大,完全可以直接内联(Inline)到Html中,这样也少一次请求。

而之后的settings.xxx.jsmain.xxx.js体积也都很小,完全也可以内联到Html中,这样可以更快的开始加载引擎之类的大头,至于这两个文件的作用是啥,查看里面代码可以知道:

  • settings.xxx.js:这里只是在window上挂了一个_CCSettings变量,里面基本也就是定义了一下入口场景以及各个bundlehash
  • main.xxx.js:是在window上挂了一个boot方法,这个方法会在引擎加载完成之后再执行

那内联之后,效果如何?

image-20211109233504030.png

可以明显看出DOMContentLoaded快了约0.6s,主场景在约5.2s左右开始加载,而所有资源加载完约需要7.5s

接下来是加载的是引擎和加载页显示的图片,这块使用通用的方式优化即可,比如对于引擎的话,那就是减少不必要的模块,而对于图片的话,就是使用工具压缩,比如tinypng(tinypng.com/)

Bundle

在这之后,可以看到3对config.xxx.jsonindex.xxx.js的加载,如果之前仔细看了main.xxx.js的话,那很容易知道这里是在加载几个Bundle的基本信息以及代码

另外,main.xxx.jsboot方法中,会优先加载internalresources这两个Bundle,这两个加载完了才开始加载mainBundle,另外这三个都是在引擎加载完才开始加载。

也就是说,从现有加载流程看,它们之间可能是有先后依赖关系的。那能否打破这层依赖关系呢?让相关资源提前加载好。因为简单来看,JSON文件完全是可以提前加载的,让浏览器缓存住,后续真实Bundle加载的时候,就会自动读取缓存,这样相当于实现了提前加载。

不过还是先仔细研究一下这几个文件中的内容是啥,看看到底有什么依赖关系。

不看不知道,一看吓一跳,你会发现internalresources下的index.xxx.js根本就是个空函数,啥也没干,但是加载了,只有main下面的index.xxx.js有注册我们在项目里面写的代码。当然这也有可能是因为我们没有在相关Bundle里面加代码导致的。这里看起来完全可以优化,那就研究一下看看怎么优化吧。

如果你看过之前的文章从Cocos支持GIF看加载机制,那就大概了解了Cocos Creator的加载过程。这里就直接看看Cocos Creator是怎么下载Bundle的

var downloadBundle = function (nameOrUrl, options, onComplete) {
    let bundleName = cc.path.basename(nameOrUrl);
    let url = nameOrUrl;
    if (!REGEX.test(url)) url = 'assets/' + bundleName;
    var version = options.version || downloader.bundleVers[bundleName];
    var count = 0;
    var config = `${url}/config.${version ? version + '.' : ''}json`;
    let out = null, error = null;
    downloadJson(config, options, function (err, response) {
        if (err) {
            error = err;
        }
        out = response;
        out && (out.base = url + '/');
        count++;
        if (count === 2) {
            onComplete(error, out);
        }
    });

    var js = `${url}/index.${version ? version + '.' : ''}js`;
    downloadScript(js, options, function (err) {
        if (err) {
            error = err;
        }
        count++;
        if (count === 2) {
            onComplete(error, out);
        }
    });
};

从上面的代码可以直观得看出,在加载Bundle的时候,其根本不关心index.xxx.js中的代码是啥,只关心其加载了,所以即使是无用的空函数,它也一样会去加载,这个就很浪费了。

不过既然它不关心index.xxx.js中的代码,那么我们完全可以预先加载有用的代码,等到引擎加载完成之后再执行即可,另外,项目中所有的代码似乎都合并到Bundle的index.xxx.js中了(未验证),其实主要关心这几个文件就行了。

再回过头来看JSON文件,之前也说过其实完全可以依赖浏览器的缓存来实现预加载,但是文件还是很碎,看这个接近空的项目,加载完,也需要加载7个JSON文件,而且有些因为太碎(体积太小)也享受不到gzip的压缩优势。

还有Cocos Creator会通过JSON文件来获取其依赖的资源,也就是说,本例子中主场景使用的那张图片,需要等主场景JSON下载好之后,发现需要一张图片,然后再去加载这张图片的描述JSON(meta元JSON🐶),最终才去加载真实的图片资源,这其实都是串行的,听起来就慢。

那我们是否可以直接合并所有JSON来预加载,等使用的时候,就完全不需要网络请求了!不过,这个也要看项目大小,如果项目太大,那可能需要更精细化的合并。

这样合并所有JS和JSON之后,就需要在boot方法中修改引擎的加载方法来支持我们的新形式

var jsonDownloader = cc.assetManager.downloader._downloaders[".json"];
var bundleDownloader = cc.assetManager.downloader._downloaders["bundle"];
var REGEX = /^(?:\w+:\/\/|\.+\/).+/;
var newJsonDownloader = function (url, options, onComplete) {
  var name = cc.path.basename(url);
  var pack = "";
  if (url.startsWith("assets/")) {
    pack = url.split("/")[1];
  }
  if (window.allJSON[pack] && window.allJSON[pack][name]) {
    onComplete && onComplete(null, window.allJSON[pack][name]);
  } else {
    jsonDownloader(url, options, onComplete);
  }
};
var newBundleDownloader = function (nameOrUrl, options, onComplete) {
  var bundleName = cc.path.basename(nameOrUrl);
  if (window.allJSON[bundleName]) {
    var version = settings.bundleVers[bundleName];
    var url = nameOrUrl;
    if (!REGEX.test(url)) url = "assets/" + bundleName;
    var out =
      window.allJSON[bundleName][
        "config." + (version ? version + "." : "") + "json"
      ];
    out && (out.base = url + "/");
    onComplete && onComplete(null, out);
  } else {
    bundleDownloader(nameOrUrl, options, onComplete);
  }
};
cc.assetManager.downloader._downloaders[".json"] = newJsonDownloader;
cc.assetManager.downloader._downloaders["bundle"] = newBundleDownloader;

看看效果:

image-20211110141946158.png

世界瞬间清净了!请求数骤减,主场景在约3.5s左右开始加载,而所有资源加载完约需要4.1s,效果拔群啊~

default_sprite_splash

请求数少了之后,你会发现这边请求了一张你完全没有使用的的图片(长宽是2x2),如果只是多请求一张图片也就算了,但它会阻塞主场景的加载!也就是说,这张完全不清楚怎么来的图片直接拖慢了加载进度。

细致研究后发现,这图片其实是internal/image下面的default_sprite_splash图片,项目中没法直接找到其依赖关系,估计是引擎依赖的某些材质资源间接引用了这张图。

题外的说下,个人也不太建议在项目中直接使用internal/image中图片,毕竟这都是单图,一方面需要额外一次网络请求加载;另一方面其也没法合并成图集,也就是DrawCall会增加。当然如果你是直接使用Texture2D(比如拖尾),而不是SpriteFrame,引用哪里的图片都一样,都无法合并成图集。

找了一圈,暂时没有找到合适的方法,不过好在这图非常小(原图82B,网络传输却需要358B),还有最后一招,那就是直接内联到Html里面,直接把引擎的图片加载流程也改了

var twoPxPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVQImWP8//8/AwMDEwMDAwMDAwAkBgMBmjCi+wAAAABJRU5ErkJggg==';
var pngDownloader = cc.assetManager.downloader._downloaders['.png'];
var newPngDownloader = function(url, options, onComplete) {
  var pngName = cc.path.basename(url);
  // 当前先写死,实际项目中优化
  if (pngName === '0275e94c-56a7-410f-bd1a-fc7483f7d14a.cea68.png') {
    cc.assetManager.downloader.downloadDomImage(twoPxPng, options, onComplete);
  } else {
    pngDownloader(url, options, onComplete);
  }
}
cc.assetManager.downloader._downloaders['.png'] = newPngDownloader;

再来看看效果:

image-20211110155848384.png

又快了0.6s!所有资源加载完约需要3.5s,看来内联是个明智的选择。

总结

上面的优化,算是Cocos开发Web小游戏的加载优化的思路,实际项目中,可能还需要更多的验证。另外,这个也只是纯技术方面的思考,实际项目中,业务的优化才是大头,毕竟使用的图片太多,轻轻松松就抹平了这个技术方面的优化。

从优化后的数据来看,8.1 / 3.5 ≈ 2.3,速度是优化前的2.3倍,当然在网络状况良好的情况下,速度提升就没有那么明显了。

本次涉及到的相关代码,在Cocos-Web在这里可以查看,有兴趣的可以看看。