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

前言

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


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

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

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

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

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


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

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

背景

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

  • 小游戏首屏启动优化
  • Cocos Creator 微信小游戏平台启动与包体优化(首屏渲染耗时降低 50%)
  • 微信小游戏的启动性能优化之首屏渲染

按照这三篇文章的逻辑来优化小游戏能够达到很理想的效果,本文是结合上述文章所作。这三篇文章均是直接使用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 

    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 
  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目录就忽略本段落内容 )

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


你可能感兴趣的:(Cocos Creator 微信小游戏平台启动性能优化之首屏渲染优化)