cover_image

秒开率从18%到64%,我们对小程序模拟器做了什么?

快手大前端技术 快手大前端技术
2025年02月25日 11:54
图片

导读


小程序是一种运行在快手生态内,无需下载安装、即用即走的轻量级应用。其中,模拟器是快手开发者所使用的工具中最核心的模块之一,但因性能问题收到开发者反馈。为此,24年Q2快手启动了模拟器性能优化专项,从线上数据看:模拟器秒开率从18%提升至64%,FCP P90从4.4s提升至1.9s。本文详细介绍优化措施和成效。

一、问题背景

图片


小程序是快手开放平台对外提供的开放能力之一, 是一种运行在快手生态内,无需下载安装、即用即走的轻量级应用。开发者以快手小程序为载体,以优质的内容、服务供给或内容生产连接用户。小程序接入流程可简单划分为四步:注册小程序、开发调试、审核上线、线上运营。其中开发调试工作主要在快手开发者工具上进行,而模拟器是快手开发者工具最核心的模块之一。


由于小程序不能直接使用浏览器环境运行,我们在开发者工具中提供了模拟器模块,模拟小程序在快手客户端的表现。

图片

模拟器是快手开发者工具中开发者使用频率最高的模块,最常见的使用场景是:修改代码=>触发编译=>模拟器刷新=>查看效果,模拟器的加载速度直接影响开发者的开发效率。


从线上数据看,模拟器性能确实比较差:FCP P90只有4.4s,秒开率只有18%,开发者也多次反馈期望优化模拟器的性能。


注:本文提到的FCP指编译完成后模拟器收到刷新事件,到小程序首次内容渲染所花的时间,秒开率指在1秒内完成FCP的比例。

二、分析解决

图片

对于性能优化,常见思路是:理清各阶段耗时->找出耗时原因->制定相应优化方案。第一步,我们先要统计各阶段耗时。


2.1 如何统计模拟器启动各阶段耗时?

图片

对于常规前端项目,很容易想到使用performance录制火焰图来分析耗时,但小程序模拟器比较特殊,无法使用performance录制。

2.1.1 为什么模拟器不能使用performance录制

快手小程序采用了双线程架构,分为逻辑层与渲染层。在模拟器中,渲染层使用Electron Webview(独立进程)承载,一个页面对应一个iframe;逻辑层使用Electron BrowserView(独立进程)承载。

图片

因为执行信息分布在两个进程中,但调试器performance录制的只能对接一个进程,这导致了不能直接使用performance录制功能,所以只能先在代码中手动打点来记录启动时各阶段耗时。


2.2 手动打点分析,确定主要优化方向

图片

通过在代码中手动打点,我们观察到容器准备阶段的耗时比较长,再加上不能用performance录制,无法细致的统计加载执行阶段中的耗时,所以一开始我们优先考虑优化容器准备阶段耗时。

图片

2.2.1 优化容器准备阶段耗时

双进程改为单进程:

在当前的模拟器方案中,每次模拟器刷新,都需要销毁并重建逻辑层容器,重新加载框架文件以及基础库(因为需要重新加载基础库、执行用户代码,重新走一遍小程序的生命周期),这些操作耗费了比较多的时间。我们的优化思路是尽可能减少需要重新加载执行的资源(进程资源、文件资源),有三个方案:

图片

三个方案运行逻辑简述如下:

BrowserView + IFrame:

图片

webworker:

图片

IFrame:

图片

综合评估:计划采用IFrame方案。

  • 从内存占用、可移植性来看,webworker、IFrame方案比较好;

  • IFrame刷新速度略快于webworker,webworker运行时性能更好:

    • webworker方案会新开一个线程,运行时性能优与IFrame,但由于是在PC端,单线程带来性能影响比起在移动端更小,也能接受。

    • 模拟器主要场景是刷新看效果,而不是操作使用模拟器中的小程序,所以刷新速度比运行时性能优先级更高

  • 从时间成本看,IFrame方案更小,且改动点在能够被webworker复用,后期即便考虑webworker方案也能成本也更小一些。


将逻辑层通过一个IFrame来承载,并且将其至于模拟器容器进程下,与页面IFrame同级,这样就可以拿掉一个进程,并且得益于同源特性,IFrame还可以复用父进程的线程资源,刷新速度会更快。

模块缓存复用:

由于逻辑层、渲染层调整后变成了同级的IFrame,可以共享parent window,在此基础上,我们可以将逻辑 / 渲染层框架文件一些比较独立的、业务无关的功能模块提取至父容器里面,通过parent window调用,以降低框架文件包体积,提升加载速度。

图片

2.3 performance录制统计更精细耗时

图片

模拟器改为单进程后带来另一个好处是能够使用调试器的performance录制功能了,因为渲染层跟逻辑层改成了IFrame来承载,处于同一个进程中,执行信息可以由调试器直接采集到。

2.3.1 编译产物优化为按需加载

通过观察火焰图,发现模拟器在启动时就将小程序代码中所有页面的编译产物加载了,这一段逻辑耗费了比较多的时间,但其实每次模拟器刷新并不需要把所有的页面编译产物都加载进来,只需加载当前页面所需编译产物即可。

图片

我们将编译产物调整为了按需加载:当基础库需要执行对应编译产物时再去加载。加载方式也由之前的script标签异步加载,替换成了readFileSync + eval(基础库限制只能使用同步方式加载),readFileSync读取文件内容,eval完成加载。


但调整为按需加载之后,发现了一个新的问题:如果断点所指向代码的执行时机比较靠前(如onLoad阶段),则有可能导致断点失效。

图片

为什么断点失效了?

经过debug,发现断点失效跟编译产物的加载方式有关。原先使用script标签加载,调试器可以将代码与script标签的src地址指向文件直接关联上,而优化后使用了eval,无法直接将代码与文件关联上,从而导致断点失效。


全量加载(优化前):

断点设置流程:第一次收到路由事件后先使用script标签全量加载所有页面编译产物,调试器根据script src属性指向的文件路径找到并通知内核设置断点。

加载流程:

图片


按需加载(优化后):

断点设置流程:eval加载执行代码时,调试器开始解析sourcemap,解析完成后通知内核设置断点。

加载流程:

图片

但我们还发现一个现象是:如果断点指向代码执行时机比较靠后,则可以断点成功。这是因为编译产物文件中有sourcemap,sourcemap解析完成后调试器也能将eval的代码跟对应文件关联上,找到对应文件相关断点并通知内核设置断点,所以此时能断点成功。

通过#sourceURL解决
问题主要原因在于,调试器无法在一开始将eval的代码与源文件关联上,虽然sourcemap解析完也能关联上,但sourcemap解析是耗时的,也就导致了调试器通知模拟器内核设置断点时,断点指向代码已经执行过了,导致断点不生效,所以不能完全依赖sourcemap。
有没有办法不依赖sourcemap的情况下能让eval与源文件直接关联上?我们在Chrome官方文档中找到了#sourceURL这个配置:

图片

过这个配置可以让调试器将eval的代码跟文件直接关联上,无需等待解析sourcemap完成,即可直接根据sourceURL配置查找并通知内核设置断点。

所以我们给源码加上了#sourceURL注释再进行eval。

图片

优化后的断点设置流程:

图片

2.4 performance录制为什么会影响模拟器性能?

图片

在使用performance录制功能时,发现开启录制状态下模拟器的启动速度要快一些。

图片

经过实测,performance录制确实让模拟器启动速度变快了,且差距明显。

关闭录制

开启录制

onReady触发耗时:1204ms

onReady触发耗时:649ms

图片图片

考虑到调试器与模拟器主要是通过CDP消息进行交互,我推测是开启录制后某条CDP消息导致了模拟器性能大幅提升。

2.4.1 CDP是什么?

CDP全称是Chrome DevTools Protocol,是供Chrome Devtools使用的一个协议,简单说下Chrome Devtools的原理:

  • 加载一个 web 页面时,浏览器会为该页面起一个Websocket  Server,在打开这个页面的 Devtools 时与该 Server 建立 Websocket 连接,以这种方式实现通信。

  • 在某些关键事件发生时(如网络请求,用户调用console api),浏览器内核会向devtools发送CDP消息;devtools也可以向浏览器内核发送消息,来命令页面执行某些操作(如在console面板中输入代码并执行)。二者之间的通信遵循Chrome Devtool Porotol。

图片

一条CDP消息示例

//移除断点消息{  "id":39,  "method":"Debugger.removeBreakpoint",  "params":{    "breakpointId":"1:207:0:page/finishAutoCase/finishAutoCase.js"  }}

2.4.2 开启performance录制后,调试器对模拟器做了什么?

我们可以通过Protocol Monitor面板看到开启录制后调试器往模拟器发送了哪些CDP消息(图中红框部分,大多是xx.disable:禁用某些功能)

图片

简单的方法是将这些消息直接挨个拦截测试一下就能知道哪些消息提升了性能,不过由于这里用的是Electron Webview自带的原生调试器,没办法直接拦截原生调试器发送至模拟器内核的CDP消息,还是需要从开发者工具自己实现的调试器入手。


但开发者工具中的调试器未实现performance功能,所以也不能直接测试。我尝试将开发者工具中调试器与原生调试器都关闭后,模拟器性能达到了开启performance录制后的效果,这能得出一个结论是「模拟器性能下降是由开发者工具调试器造成的」。

2.4.3 优化调试器相关逻辑

经过调试,我们发现可以对调试器的部分CDP消息做相关优化:对一些在启动阶段用不上的调试器功能,先暂时关闭(缓存相关CDP消息),等实际用到对应功能或模拟器加载完成时再打开(发送所有缓存消息),来达到提升模拟器加载速度的效果。


最终方案如下图所示:

出于稳定性考虑,我们也为这个优化加了开关

图片

三、总结

图片

本文介绍了我们在对模拟器进行性能优化过程中,做了哪些事情。首先通过手动打点分析耗时,确定了主要优化方向,将模拟器的双进程架构改成了单进程架构。在单进程架构下,通过增加缓存复用层,进一步提升了加载速度。同时单进程架构也使得我们可以使用performance录制工具进行更精细的耗时分析,针对性的对编译产物做了按需加载优化,并通过「#sourceURL注释」解决了断点失效的问题。此外,我们也对调试器相关逻辑进行了优化,并取得不错的效果。

视频加载失败,请刷新页面再试

刷新

经过本次优化后,模拟器秒开率从18%提升至64%FCP P90从4.4s提升至1.9s,在开发者满意度调研中也获得了好评。模拟器的性能与开发者体验、开发效率息息相关,而提高开发者的开发体验与开发效率,是我们团队的首要任务。未来,我们将继续努力,不断优化和完善模拟器的各项功能,为开发者提供更好的支持。


- END -

解码快手大前端技术 · 目录
上一篇动效资源交付的突破:Vision平台准入准出方案下一篇快手前端通用静态托管服务KFX演进历程:从崎岖土路到平坦高速
继续滑动看下一个
快手大前端技术
向上滑动看下一个