最近几个月在公司里做 Scratch 的研发工作,主要负责性能优化这一块.
如果你没听过 Scratch, 它是麻省理工开发的的一款图形化编程的 web 应用, 你可以在网页上通过拼接代码块来开发一些游戏应用
Scratch 也被认为是一种编程语言, 很多做少儿编程教育的公司会基于 Scratch 进行教学
这里有一个用 Scratch 实现的应用,建议先玩一下
我司的安卓app是基于 chromium 来加载 web 应用的
在一些低算力的平板上(点名小度学习机😂), scratch 工程甚至会加载一分钟的时间, 非常影响用户体验. 在经过一系列设计优化后, 我把线上的 scratch 工程的平均加载耗时降低了 60%, 这里跟大家分享下一些思路和心得
Scratch 是如何加载工程文件的
下载 sb3 文件
Scratch 首先会去下载一个后缀为 .sb3 的文件,里面包含了一个 scratch 工程的所有信息
虽然后缀是 .sb3, 但是这个文件实际上就是一个 zip 文件, 你可以把后缀改成 .zip, 就可以正常解压这个文件. 解压过后, 里面是一堆图片和音频文件还有一个名为 project.json 的文件,如下图所示
加载角色对应的资源
当 Scratch 把这个文件下载完之后, 它会根据 project.json 的文件来加载所有的资源, project.json 里面是各个角色和他们对应的图片和音频的 md5 值
可以看到 costumes 就是它所有的的图片,sounds 就是它所有的音频
scratch 通过循环替换这个角色的图片,播放它的声音, 就可以做出来角色唱唱跳跳的游戏应用
除此之外,Scratch 还会对这些资源文件进行 md5 后再存入 scratch-storage
对于 svg图片,Scratch 会对它的内容进行处理和修正,为了保证后续能正确加载并渲染 svg 图片
通过 Profile ,可以非常直观的看到 Scratch 加载的具体耗时都发生在哪里
在 Scratch 加载的时候,大量出现了上面的函数调用,这个函数调用来源于 jszip ,也就是解压.sb3(zip) 文件时调用的
第二个耗时的函数调用是 md5, 每个资源在存入 scratch-storage 的时候都产生了 md5计算
总结并梳理一下上面的信息
下面是 Scratch 加载工程文件中比较耗时的步骤
- 下载
.sb3文件 - 用
jszip解压.sb3文件 - 对每个资源做
md5 - 解析并重新生成
svg的数据,scratch专门为这个逻辑写了个库 scratch-svg-renderer scratch-render使用webgl对图片进行二维纹理图像生成 (textureImage2d),用于后面渲染图片
上面的哪些步骤可以优化呢?读者可以简单思考一下这个问题
如何优化
流式的文件格式
对于问题1,我们似乎没有什么处理手段,下载就一定会有下载时间的。
先看问题2,如何避免 unzip 解压的过程。
http 协议本身就支持gzip等压缩算法传输数据,没有必要在js里面进行解压过程。 浏览器原生来做解压肯定比在js里面做解压要快的多
这里的思路是,设计一种新的文件格式编码所有的文件,本身不做压缩处理,在 http 传输的过程中通过 gzip传输。保证体积不变的同时,利用浏览器原生代码解压,这样更快。
我借鉴了flv的编码格式,设计了一套新的编码格式
文件会由若干个 chunk 组成, 每个 chunk 都由一个 header 和一个 data 组成
header 里面描述后面跟随的 data 的大小和类型,就像一个单链表一样。
data 就是 svg|png|mp3|wav 等文件数据
这样做有2个好处
- 不再是
zip格式的文件,没有在js里面解压的负担, 只需要在网络传输的时候gzip就可以了 - 文件是流式的,可以一边下载一边解析
现在浏览器支持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) | 这一步似乎没有办法?? |
一个比较形象的流程图如下所示
理想状态下,下载结束的同时加载也并行完成
结果
最后统计了一下上线后的数据,平均加载耗时降低了 60%,完美!
但是还有一个问题 scratch 利用 webgl 对图片处理的步骤能不能也略过?其实也是可以的,但是稍微有点复杂,可能需要另外一片文章才能解释清楚
欢迎大家在评论区友好交流,谈谈自己的想法😁