我們最近推出了一些網頁應用程式檢查器的重要改進——這項功能已包含在您的 Sketch 訂閱中,無需額外付費。其中最大的變化之一是顯著的效能提升。
我們最大的關注點始終是為您提供最佳體驗。我們的用戶每天都在挑戰 Sketch 的極限,這無可避免地意味著更大、更複雜的文件。因此,我們努力改進檢查器的載入時間,以幫助每個人更有效率地在瀏覽器中檢查設計,並為我們自己提供更多空間來開發新功能。現在,我們想深入探討我們是如何做到這一點背後的技術故事。
著重於預處理
為了盡可能保持檢查器的效能,我們會預先處理 Sketch 檔案(透過 AWS lambda 函式),只提供我們需要的數據。Sketch 文件可能很大,但因為您一次只檢查一個畫板,所以我們可以只載入您需要的數據,並保持互動的效能。這個預處理步驟正是我們想要加速的部分。
想要提升效能,首先要做的事情就是測量它。我們網頁版檢查器的預處理器是用 Go 語言編寫的,幸運的是,Go 語言本身就提供了很棒的效能分析工具。如果您的應用程式中已經有基準測試,Go 語言可以非常輕鬆地分析 CPU 使用率。
go test -bench BenchmarkParse -cpuprofile out.pprof
產生的檔案 (out.pprof
) 然後可以使用 go tool pprof out.pprof
進行分析。
解析 JSON
關於有多少效能瓶頸問題歸結於某人在某處解析 JSON,這可能可以拿來開個玩笑。由於我們的檔案格式基本上是一個包含 JSON 檔案的資料夾(以及一個開放的規格——我們將在未來的文章中詳細說明),這也是我們今天要討論的核心內容。
我們發現大部分的載入時間都花在各種 JSON 解析函式中(使用 jsoniter),而不是之後使用這些數據進行的處理。這並不令人意外;我們使用的測試文件包含 750MB 的未壓縮 JSON 數據,將其從純文字解析回某種結構需要時間。
如同先前提到的,由於網頁應用程式一次只檢視一個 Artboard,我們不需要解析整個 Sketch 文件。基於這個想法,我們最初的做法是將 JSON 解析成通用的 map[string]interface{}
。如此一來,我們可以在進一步處理前,直接忽略不需要使用的 Artboard 資料,將需要處理的 JSON 資料量減少到原本的一小部分。接著,我們使用 mapstructure 將剩下的資料轉換成前端可以直接使用的有意義的 struct
。
驗證我們的假設
當我們第一次進行這個優化時,直覺認為這是正確的決定。但用實際數據驗證假設總是有益的,對吧?為了確定解析成 map 或 struct
的速度,我們撰寫了測試程式來測量兩者。

結果顯示,直接將 JSON 解碼成 struct
(2.23 秒)比先轉換成 map(6.51 秒)快了將近 3 倍。很明顯地,我們的第一個任務就是移除 mapstructure 函式庫。這樣我們就可以直接將所有資料解析成 struct
,然後再去除不需要的頁面和 Artboard。重寫 Lambda 函式的大部分程式碼後,我們準備進行初步比較。

結果——從 **5.27 秒** 縮短到 **3.35 秒**——速度提升非常顯著!但我們正處於順風期,無法就此打住。或許我們可以利用 Go 語言著名的並行特性來進一步提升速度?
可惜的是…效果不彰。大多數情況下,網頁檢視器 Lambda 函式只處理 Sketch 文件中的一個 JSON 檔案——畢竟,我們一次只檢視一個頁面或 Artboard。我們不願承認失敗,於是將注意力轉向主要的 document.json
。這個檔案包含我們需要參考的共享文字和圖層樣式——或許這裡可以快速獲得一些改善?幾行程式碼後,結果顯示節省的時間…略少於 8 毫秒。好吧,並非每個想法都能產生完美的結果!
解析點座標
由於並行特性無法幫助我們,我們再次回到效能分析器,發現許多與正規表達式相關的函式。我們知道在解析點座標時使用了一個正規表達式——我們將它們表示為 "{x, y}"
字串,使用正規表達式解析並轉換成浮點數。
re := regexp.MustCompile(`{([\\w\\.\\-]+),\\s?([\\w\\.\\-]+)}`)
parts := re.FindAllStringSubmatch(pointString, -1)
x, _ := strconv.ParseFloat(parts[0][1], 64)
y, _ := strconv.ParseFloat(parts[0][2], 64
return Point{X: x, Y: y}
檢視我們的大型測試文件,我們注意到它包含超過一百萬個點——大量的圖層,以及描述向量路徑的座標和點。這感覺像是另一個潛在的瓶頸。正規表達式非常方便,但並非總是速度最快,因此我們移除正規表達式,改用手工編寫的字串解析。
var point Point
pointString = strings.TrimLeft(pointString, "{")
pointString = strings.TrimRight(pointString, "}")
parts := strings.Split(pointString, ",")
x, _ := strconv.ParseFloat(parts[0], 64)
y, _ := strconv.ParseFloat(parts[1], 64)
point.X = x
point.Y = y
return point
速度略有提升——但我們可以做得更好。就在此時,靈感湧現:我們意識到許多點座標是相同的。為什麼呢?因為 Sketch 檔案格式使用單位座標描述所有向量點(座標系範圍從 {0,0} 到 {1,1})。所以我們檢查了一下,確實,在我們的測試文件中,幾乎 70% 的點都是「{0, 0}」、「{0, 1}」、「{1, 0}」和「{1, 1}」。這是個好消息——這意味著我們可以耍點小聰明!
var point Point
switch pointString {
case "{0, 0}":
point.X = 0
point.Y = 0
case "{1, 0}":
point.X = 1
point.Y = 0
// [...]
default:
// parse string
}
但這樣做有產生差異嗎?事實證明,「耍小聰明」對效能提升非常有幫助。執行時間減少了 560 毫秒,從 3.34 秒縮短到 2.78 秒。現在的處理速度幾乎是最初的兩倍。

部署變更
我們認為這是個適當的時機,可以停下腳步,在實際硬體(在本例中為 M1 MacBook Air)上測試效能改進的情況,而不是在我們的開發機器上。我們在伺服器端使用許多 Mac 來處理 Sketch 文件,但在 AWS 上執行 Linux 的部分則更多。

最後,我們在 AWS 測試伺服器上運行了一項測試,並將結果與舊程序進行了比較。改進很明顯,證實了我們之前所有測試的結果(我們在 M1 MacBook Air 上執行的測試)。
我們對結果感到非常滿意,執行速度提高了 2.3 倍,記憶體使用量提高了 3.7 倍,甚至超出了我們的預期。在經過一輪虛擬擊掌慶祝之後,我們將其推廣給所有使用者。您可以清楚地看到我們切換到新程式碼的那一刻

很少有文件像我們在所有這些測試中使用的文件那麼大和複雜,但在進行這樣的改進時,使用極端案例通常很有用。我們對指標的觀察顯示,p99 延遲(除 1% 的異常值外,所有文件處理所需的時間平均值)從 **6.25 秒**下降到 **1.97 秒**——穩固的 3.2 倍改進。
我們現在處理絕大多數 Sketch 文件的時間都 **不到兩秒**。考慮到其中幾百毫秒花費在從我們的儲存伺服器下載 Sketch 檔案上,我們對這個結果感到非常滿意。我們希望您已經注意到日常工作中的改進,並且這些改進讓您在 Sketch 中的工作更加順暢。