Cocos Creator 微信小游戏平台启动性能优化之首屏渲染优化

前言

微信小游戏云测试服务的开放之后,越来越多的开发者使用该功能测试自己的小游戏的性能。但是大部分小游戏的测试结果显示,启动性能得分很低,远远达不到80分的标准线,甚至难以达到60分。


根据微信小游戏文档中的启动优化最佳实践,优化思路一共有6种:

  • 精简首包资源
  • 分包加载
  • 引擎插件
  • 预下载能力
  • 降低首屏渲染资源
  • 尽快渲染。

常规的优化思路往往是两步:

    1. 拆分代码包,精简首包资源,使得首包只存首屏图片和一个加载进度条及相关代码;
    1. 使用分包加载。

根据小游戏的启动时序,会发现,降低代码包资源会减少了代码包下载,以及在某种程度下降低JS注入耗时。


自然而然,解决思路无非是,1. 降低注入代码的大小来减少JS注入耗时; 2. 简化首屏渲染逻辑,比如不依赖第三方引擎进行轻量渲染。但是,怎么做呐?

本文,将结合上面的两个解决思路,提供一套不依赖引擎(WebGL/Canvas2D直接渲染首屏)的小游戏首包加载套路,即减少了引擎代码注入耗时,又避免了第三方引擎的重度渲染。该方式可以直接套用,使用后的小游戏的启动性能在云测试报告下的启动性能得分能达到80及以上。

背景

目前微信官方文档、微信小游戏社区和各个引擎社区已经有很多篇关于启动优化的文章。除了微信官方文档,在微信小游戏社区和cocos 社区下有三篇非常优秀的使用WebGL渲染首屏的文章:

按照这三篇文章的逻辑来优化小游戏能够达到很理想的效果,本文是结合上述文章所作。这三篇文章均是直接使用WebGl渲染首屏,能够达到很理想的效果。强烈先去看一眼这三篇文章及下面的评论,基本上遇到的所有问题都有解答。

方法(自行渲染首屏并在分包加载引擎)

分为两个部分:

一是针对代码注入和首屏渲染的优化,将会在引擎加载之前自行渲染出一张首屏图片,并且修改引擎的 Mini-game-adapters 来兼容引擎的渲染代码。

二是针对代码包的优化,将会把引擎相关的几乎所有内容放在子包中进行加载,只留下必要的首屏代码。

自行渲染首屏图片

1.在项目中创建 build-templates 目录,再创建 wechatgame 目录以准备自定义发布模版(官方介绍 72

2.在 wechatgame 目录下新建 webgl_first_render.js 脚本,拷贝以下内容:

var VSHADER_SOURCE =
    'attribute vec4 a_Position;\n' +
    'attribute vec2 a_TexCoord;\n' +
    'varying vec2 v_TexCoord;\n' +
    'void main() {\n' +
    '  gl_Position = a_Position;\n' +
    '  v_TexCoord = a_TexCoord;\n' +
    '}\n';

var FSHADER_SOURCE =
    '#ifdef GL_ES\n' +
    'precision mediump float;\n' +
    '#endif\n' +
    'uniform sampler2D u_Sampler;\n' +
    'varying vec2 v_TexCoord;\n' +
    'void main() {\n' +
    '  gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +
    '}\n';

const VERTICES = new Float32Array([
      -1, 1, 0.0, 1.0,
      -1, -1, 0.0, 0.0,
      1, 1, 1.0, 1.0,
      1, -1, 1.0, 0.0,
    ]);

var INITENV =  false;
var TEXTURE, USAMPLE, IMAGE;

function initShaders(gl, vshader, fshader) {
    var program = createProgram(gl, vshader, fshader);
    if (!program) {
        console.log('Failed to create program');
        return false;
    }

    gl.useProgram(program);
    gl.program = program;

    return true;
}

function createProgram(gl, vshader, fshader) {
    // Create shader object
    var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
    var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
    if (!vertexShader || !fragmentShader) {
        return null;
    }

    // Create a program object
    var program = gl.createProgram();
    if (!program) {
        return null;
    }

    // Attach the shader objects
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);

    // Link the program object
    gl.linkProgram(program);

    // Check the result of linking
    var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!linked) {
        var error = gl.getProgramInfoLog(program);
        console.log('Failed to link program: ' + error);
        gl.deleteProgram(program);
        gl.deleteShader(fragmentShader);
        gl.deleteShader(vertexShader);
        return null;
    }
    return program;
}

function loadShader(gl, type, source) {
    // Create shader object
    var shader = gl.createShader(type);
    if (shader == null) {
        console.log('unable to create shader');
        return null;
    }

    // Set the shader program
    gl.shaderSource(shader, source);

    // Compile the shader
    gl.compileShader(shader);

    // Check the result of compilation
    var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!compiled) {
        var error = gl.getShaderInfoLog(shader);
        console.log('Failed to compile shader: ' + error);
        gl.deleteShader(shader);
        return null;
    }

    return shader;
}

function initVertexBuffers(gl, vertices) {
    var verticesTexCoords = vertices || new Float32Array([
        // Vertex coordinates, texture coordinate
        -1, 1, 0.0, 1.0,
        -1, -1, 0.0, 0.0,
        1, 1, 1.0, 1.0,
        1, -1, 1.0, 0.0,
    ]);

    var n = 4; // The number of vertices

    // Create the buffer object
    var vertexTexCoordBuffer = gl.createBuffer();
    if (!vertexTexCoordBuffer) {
        console.log('Failed to create the buffer object');
        return -1;
    }

    // Bind the buffer object to target
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

    var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
    //Get the storage location of a_Position, assign and enable buffer
    var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if (a_Position < 0) {
        console.log('Failed to get the storage location of a_Position');
        return -1;
    }
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
    gl.enableVertexAttribArray(a_Position);  // Enable the assignment of the buffer object

    // Get the storage location of a_TexCoord
    var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
    if (a_TexCoord < 0) {
        console.log('Failed to get the storage location of a_TexCoord');
        return -1;
    }
    // Assign the buffer object to a_TexCoord variable
    gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
    gl.enableVertexAttribArray(a_TexCoord);  // Enable the assignment of the buffer object

    return n;
}

function initTextures(gl, n, imgPath) {
    var texture = gl.createTexture();   // Create a texture object
    if (!texture) {
        console.log('Failed to create the texture object');
        return [null, null, null, false];
    }

    // Get the storage location of u_Sampler
    var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
    if (!u_Sampler) {
        console.log('Failed to get the storage location of u_Sampler');
        return [null, null, null, false];
    }
    var image = wx.createImage();  // Create the image object
    if (!image) {
        console.log('Failed to create the image object');
        return [null, null, null, false];
    }
    // Register the event handler to be called on loading an image
    image.onload = function () { 
        loadTexture(gl, n, TEXTURE, u_Sampler, image); 
        IMAGE = image;
    };
    // Tell the browser to load an image
    image.src = imgPath;
    return [texture, u_Sampler, true];
}

function loadTexture(gl, n, texture, u_Sampler, image) {
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // Flip the image's y axis
    // Enable texture unit0
    gl.activeTexture(gl.TEXTURE0);
    // Bind the texture object to the target
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // Set the texture parameters
    // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    // Set the texture image
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);

    // Set the texture unit 0 to the sampler
    gl.uniform1i(u_Sampler, 0);

    gl.clear(gl.COLOR_BUFFER_BIT);   // Clear <canvas>

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // Draw the rectangle
}

function InitGLEnv(imgPath, gl, vertices) {
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return [-1];
  }

  // Initialize shaders
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
      console.log('Failed to intialize shaders.');
      return -1;
  }

  // Set the vertex information
  var n = initVertexBuffers(gl, vertices);
  if (n < 0) {
      console.log('Failed to set the vertex information');
      return -1;
  }

  // Specify the color for clearing <canvas>
  gl.clearColor(1.0, 1.0, 1.0, 1.0);

  // Set texture
  var ret = true;
  var teture, u_sample;
  [teture, u_sample, ret] = initTextures(gl, n, imgPath);
  if (!ret) {
      console.log('Failed to intialize the texture.');
      return -1;
  }
  return [teture, u_sample, ret];
}

function drawImg(imgPath, gl) {
    if (!INITENV) {
      var r = 0;
      [TEXTURE, USAMPLE, r] = InitGLEnv(imgPath, gl, VERTICES);
      if (r < 0) {
        return;
      }
      INITENV = true;
    }
    var n = initVertexBuffers(gl, VERTICES);
    if (n < 0) {
      console.log('Failed to set the vertex information');
      return;
    }
    if (IMAGE) {
        loadTexture(gl, n, TEXTURE, USAMPLE, IMAGE);
    }
}

exports.drawImg = drawImg;

3.拷贝首屏图片到同目录下,重命名为 first.jpg

4.再创建一个脚本 game-backup.js ,拷贝以下内容:

const { drawImg } = require('./webgl_first_render.js');

function render() {
  if (FIRSTRENDER) {
    drawImg('first.jpg', gl);
    requestAnimationFrame(render);
  } 
}

wx.setPreferredFramesPerSecond(30);
var FIRSTRENDER = true;
GameGlobal.dycc = wx.createCanvas();
GameGlobal.screencanvas = GameGlobal.dycc || new _Canvas2.default()
const { screenWidth, screenHeight } = wx.getSystemInfoSync();
GameGlobal.dycc.width = screenWidth;
GameGlobal.dycc.height = screenHeight;
var gl = GameGlobal.dycc.getContext("webgl",{stencil:true});//不加{stencil:true}开发者工具上会出现白屏

requestAnimationFrame(render);

// 加载引擎代码写在这后面
const loader = require("./engine-loader");
loader.loadEngine("engine",()=>{FIRSTRENDER = false;});//此处设置FIRSTRENDER 为false是为了防止子包加载完成后一直在渲染导致黑屏

5.再创建一个json文件 game.json ,拷贝以下内容:

{
    "deviceOrientation": "landscape",
    "networkTimeout": {
        "request": 5000,
        "connectSocket": 5000,
        "uploadFile": 5000,
        "downloadFile": 5000
    },
    "subpackages": [
        {
            "name":"engine",
            "root":"engine/"
        }
    ]
}
引擎放入子包加载

把首屏渲染优化到极致之后,启动耗时中的大头就是代码包加载和代码注入这两个阶段,在不修改引擎的情况下,建议的优化手段是裁剪引擎模块,只保留首屏代码,其他代码用引擎自带的 Asset Bundle 把其他业务代码放在子包延迟加载,虽然这么做的话可能需要对项目进行大改动,并且引擎代码导致的加载和注入耗时无法优化,但这是从本质上解决问题。

下面会介绍在首屏渲染优化之后的基础上再把引擎放入子包进行加载的操作步骤,但在实际项目中可根据自身情况只做两个优化中的一个,如果把引擎放入子包,那么当前受小游戏平台的限制,引擎分离插件就不能使用了,不过子包第一次加载后即会缓存,不用担心。

这部分优化在做好构建模版后,每次构建都需要把引擎相关文件放入子包目录还是比较麻烦的,如果是开发调试,构建后可以不使用 game-backup.js 替换引擎本身的 game.js 而是像往常一样直接打包上传,这样虽然不会让首屏渲染和子包引擎优化生效,但能节省开发调试的时间。也推荐大家尝试自定义构建流程来自动化这部分工作。

6.依然打开 build-templates/wechatgame 目录,新建脚本文件 engine-loader.js ,拷贝以下内容:

function loadEngine(sub_name,cb) {
  if (wx.loadSubpackage) {
    _load(sub_name).then((result) => {
      if (!result) {
        loadEngine(sub_name);
      }else{
        cb();//此处为了执行FIRSTRENDER = false;语句
      }
    });
  } else {
    require(sub_name + '/game.js');
  }
}

function _load(sub_name) {
  return new Promise((resolve, reject) => {
    const t = new Date().getTime();

    const loadTask = wx.loadSubpackage({
      name: sub_name,
      success: function (res) {
        console.log("引擎子包加载完毕", new Date().getTime() - t, "ms");
        resolve(true);
      },
      fail: function (res) {
        console.log("引擎子包加载失败", new Date().getTime() - t, "ms");
        resolve(false);
      }
    });

    loadTask.onProgressUpdate(res => {

    });

  });
}

const _loadEngine = loadEngine;
export { _loadEngine as loadEngine };

这部分是加载子包的代码,已经按照官方文档做了旧微信基础库兼容。

7.在 wechatgame 目录下创建 engine 目录,此为引擎子包目录,并在目录内新建脚本文件 game.js,拷贝以下内容:

console.error("请把引擎相关文件放入子包");

避免忘记把引擎文件放入该目录,所以新建这个默认脚本来提示错误。

完成以上步骤后,你的目录结构应该和下面的一致


至此,构建模版就完成了,以后构建就不需要重复以上的步骤,因为使用了自定义构建模版,以后构建后只需要重复第8步的替换工作即可。

8.构建项目后,把下面几个引擎的文件夹和文件移动到 engine 目录下,然后重命名 game-backup.js 为 game.js

  • adapter.min.js
  • ccRequire.js
  • cocos
  • game.js
  • main.js
  • src

移动上面几个后还会剩下引擎的 assets 目录,由于读取资源时引擎不会读子包目录内的资源,所以需要拷贝该目录到 engine 目录,也就是主包和子包都有一个 assets 目录,然后删除主包中 assets 目录的 index.jsindex.js.map 代码相关文件,删除子包中 assets 目录的 config.xxx.json import native 的资源相关的文件,也就是主包留下 assets 的资源文件,子包留下 assets 的代码文件。 ( 若没有assets目录就忽略本段落内容 )

做完以上步骤则完成了所有工作,赶紧云测试一下,启动性能明显得到了优化,但是运行性能却有所降低,运行性能的优化在后面的篇章中会实践分享给大家


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容