秒開率從 18% 到 64%,我們對小程序模擬器做了什么? 原創
小程序是一種運行在快手生態內,無需下載安裝、即用即走的輕量級應用。其中,模擬器是快手開發者所使用的工具中最核心的模塊之一,但因性能問題收到開發者反饋。為此,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 錄制確實讓模擬器啟動速度變快了,且差距明顯。


考慮到調試器與模擬器主要是通過 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 消息示例

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,在開發者滿意度調研中也獲得了好評。模擬器的性能與開發者體驗、開發效率息息相關,而提高開發者的開發體驗與開發效率,是我們團隊的首要任務。未來,我們將繼續努力,不斷優化和完善模擬器的各項功能,為開發者提供更好的支持。

















