跳過導航
A 3D illustration of arrows pointing upwards around a 3D cloud, on a blue background.
Sketch 內部

技術講座: 加速網頁應用程式中檢查器的速度

我們深入探討了最近網頁版檢查器的效能提升,並說明我們如何達到 2.3 倍速的提升。

我們最近推出了一些網頁應用程式檢查器的重要改進——這項功能已包含在您的 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 的速度,我們撰寫了測試程式來測量兩者。

A graph comparing the time taken to process a file when parsing a map and parsing a script. Parsing a script is 2.9x faster at 2.23 seconds.

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

A graph comparing the time taken to process a real document when parsing a map and parsing a script. Parsing a script is 1.6x faster at 3.35 seconds.

結果——從 **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 秒。現在的處理速度幾乎是最初的兩倍。

A graph comparing the time taken to process a real document when parsing a map, parsing a script, and parsing a script and then parsing individual points. The combined improvements are 1.9x faster than the original method at 2.78 seconds.

部署變更

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

Two graphs comparing the time taken to process a document and the memory usage difference between the old method and the new method. The new method is 2.3x faster and uses 3.7x less memory.

最後,我們在 AWS 測試伺服器上運行了一項測試,並將結果與舊程序進行了比較。改進很明顯,證實了我們之前所有測試的結果(我們在 M1 MacBook Air 上執行的測試)。

我們對結果感到非常滿意,執行速度提高了 2.3 倍,記憶體使用量提高了 3.7 倍,甚至超出了我們的預期。在經過一輪虛擬擊掌慶祝之後,我們將其推廣給所有使用者。您可以清楚地看到我們切換到新程式碼的那一刻

A graph showing the real-world processing time of documents before and after the new method is introduced into production. The new method cuts the peak time down from around 19 seconds to around 7 seconds.

很少有文件像我們在所有這些測試中使用的文件那麼大和複雜,但在進行這樣的改進時,使用極端案例通常很有用。我們對指標的觀察顯示,p99 延遲(除 1% 的異常值外,所有文件處理所需的時間平均值)從 **6.25 秒**下降到 **1.97 秒**——穩固的 3.2 倍改進。

我們現在處理絕大多數 Sketch 文件的時間都 **不到兩秒**。考慮到其中幾百毫秒花費在從我們的儲存伺服器下載 Sketch 檔案上,我們對這個結果感到非常滿意。我們希望您已經注意到日常工作中的改進,並且這些改進讓您在 Sketch 中的工作更加順暢。

您可能也會喜歡

免費試用 Sketch

無論您是 Sketch 的新手,還是回來看看有什麼新功能,我們都會在幾分鐘內讓您完成設定,並準備好開始創作您的最佳作品。

免費開始使用
免費開始使用