性能优化-把 Scratch 的加载用时降低 60%

845 阅读5分钟

最近几个月在公司里做 Scratch 的研发工作,主要负责性能优化这一块.

如果你没听过 Scratch, 它是麻省理工开发的的一款图形化编程的 web 应用, 你可以在网页上通过拼接代码块来开发一些游戏应用

Scratch 也被认为是一种编程语言, 很多做少儿编程教育的公司会基于 Scratch 进行教学

这里有一个用 Scratch 实现的应用,建议先玩一下

我司的安卓app是基于 chromium 来加载 web 应用的

在一些低算力的平板上(点名小度学习机😂), scratch 工程甚至会加载一分钟的时间, 非常影响用户体验. 在经过一系列设计优化后, 我把线上的 scratch 工程的平均加载耗时降低了 60%, 这里跟大家分享下一些思路和心得

Scratch 是如何加载工程文件的

下载 sb3 文件

Scratch 首先会去下载一个后缀为 .sb3 的文件,里面包含了一个 scratch 工程的所有信息

虽然后缀是 .sb3, 但是这个文件实际上就是一个 zip 文件, 你可以把后缀改成 .zip, 就可以正常解压这个文件. 解压过后, 里面是一堆图片和音频文件还有一个名为 project.json 的文件,如下图所示

image.png

加载角色对应的资源

当 Scratch 把这个文件下载完之后, 它会根据 project.json 的文件来加载所有的资源, project.json 里面是各个角色和他们对应的图片和音频的 md5

截屏2024-12-04 14.27.16.png

可以看到 costumes 就是它所有的的图片,sounds 就是它所有的音频

scratch 通过循环替换这个角色的图片,播放它的声音, 就可以做出来角色唱唱跳跳的游戏应用

除此之外,Scratch 还会对这些资源文件进行 md5 后再存入 scratch-storage

对于 svg图片Scratch 会对它的内容进行处理和修正,为了保证后续能正确加载并渲染 svg 图片

通过 Profile ,可以非常直观的看到 Scratch 加载的具体耗时都发生在哪里

image.png

Scratch 加载的时候,大量出现了上面的函数调用,这个函数调用来源于 jszip ,也就是解压.sb3(zip) 文件时调用的

image.png

第二个耗时的函数调用是 md5, 每个资源在存入 scratch-storage 的时候都产生了 md5计算

总结并梳理一下上面的信息

截屏2024-12-04 15.39.07.png

下面是 Scratch 加载工程文件中比较耗时的步骤

  1. 下载 .sb3 文件
  2. jszip 解压 .sb3 文件
  3. 对每个资源做 md5
  4. 解析并重新生成 svg 的数据,scratch 专门为这个逻辑写了个库 scratch-svg-renderer
  5. scratch-render 使用 webgl 对图片进行二维纹理图像生成 (textureImage2d),用于后面渲染图片

上面的哪些步骤可以优化呢?读者可以简单思考一下这个问题

如何优化

流式的文件格式

对于问题1,我们似乎没有什么处理手段,下载就一定会有下载时间的。

先看问题2,如何避免 unzip 解压的过程。

http 协议 本身就支持 gzip 等压缩算法传输数据,没有必要在 js 里面进行解压过程。 浏览器原生来做解压肯定比在 js 里面做解压要快的多

这里的思路是,设计一种新的文件格式编码所有的文件,本身不做压缩处理,在 http 传输的过程中通过 gzip传输。保证体积不变的同时,利用浏览器原生代码解压,这样更快。

我借鉴了flv的编码格式,设计了一套新的编码格式

截屏2024-12-04 16.19.39.png

文件会由若干个 chunk 组成, 每个 chunk 都由一个 header 和一个 data 组成

header 里面描述后面跟随的 data 的大小和类型,就像一个单链表一样。

data 就是 svg|png|mp3|wav 等文件数据

这样做有2个好处

  1. 不再是 zip 格式的文件,没有在 js 里面解压的负担, 只需要在网络传输的时候 gzip 就可以了
  2. 文件是流式的,可以一边下载一边解析

现在浏览器支持stream的形式处理 http response

fetch().then(resp=>resp.body.getReader())

也就是说,没必要等完全下载完了再开始解析的过程,只要第一个 chunk 下载完了,就可以先拿到一个 chunk 做后续加载工作

回到问题1,虽然没有办法减少下载的时长,但是如果设计了这套格式,就可以一边下载一边做后续解析的工作了 , 并行化下载和加载

伪代码如下所示

const reader = await getResponseStreamReader()

while (!reader.done){
  // 读 header
  const header = await reader.read(headerByteLength)
  // 读文件数据
  const fileData = await reader.read(header.dataByteLength)
}

对于问题3,所有的 chunk data 都计算好 md5 放在 header 里面

对于问题4,所有的 svg data 都先通过scratch-svg-renderer处理过再放在文件里面

这样只要写好一个编码器,还有一个解码器就可以了

总结一下

对于每个问题和对应的解决方案

问题解决方案
下载 .sb3 文件的耗时用流的形式处理文件,并行化下载和加载,充分利用下载的时间
jszip 解压 .sb3 文件放弃 zip 的文件格式, 在 http 传输的时候 gzip 即可, 这样js代码只要读 ArrayBuffer 就行了
md5算好 md5 放在 header 里面, 加载的时候直接读出来用
解析 svg所有的 svg data 都先通过scratch-svg-renderer处理过再放在文件里面
对图片进行二维纹理图像生成 (textureImage2d)这一步似乎没有办法??

一个比较形象的流程图如下所示

截屏2024-12-04 16.55.09.png

理想状态下,下载结束的同时加载也并行完成

结果

最后统计了一下上线后的数据,平均加载耗时降低了 60%,完美!

但是还有一个问题 scratch 利用 webgl 对图片处理的步骤能不能也略过?其实也是可以的,但是稍微有点复杂,可能需要另外一片文章才能解释清楚

欢迎大家在评论区友好交流,谈谈自己的想法😁