[譯] 我們是怎樣把 Carousell 的移動端 Web 體驗搞快了 3 倍的?
Carousell 是一個在新加坡開發的移動分類廣告市場,並在包括印度尼西亞、馬來西亞和菲律賓在內的許多東南亞國家開展業務。我們在今年年初為一批使用者推出了我們移動 Web 端的 ofollow,noindex">漸進式網頁應用(PWA) ] 版本。
在本文中,我們將分享 (1) 我們想要建立更快的 Web 端體驗的動機,(2) 我們怎麼完成它,(3) 它對我們使用者的影響,以及 (4) 是什麼幫助了我們快速完成。

這個 PWA 在mobile.carousell.com :mag_right:
為什麼一定要有更快的 Web 體驗?
我們的應用是為新加坡市場開發的,我們已經習慣於使用者擁有高於平均水平的手機和高速的網際網路。然而,隨著我們擴充套件到整個東南亞地區的更多國家,如印度尼西亞和菲律賓,我們面臨著提供同樣令人愉快和快速的網路體驗的挑戰。原因是,在這些地方,較一般的終端裝置 和網際網路速度 與我們的應用設計標準相比,往往速度慢並且不太可靠。
我們開始閱讀更多有關效能的內容,並開始使用Lighthouse 重新審視我們的應用,我們意識到 如果我們想要在這些新的市場中成長 , 我們需要更快的 Web 體驗 。 如果我們想要獲取或是留住我們的使用者,那麼一個網頁在 3G 網路下(跟我們一樣)需要載入超過 15 秒就是不能接受的了。

Lighthouse 的效能表現得分會是一個很好的叫醒服務~ :house:
Web 端通常是我們的新使用者發現和了解 Carousell 的入口。 我們想從一開始就給他們一個愉快的體驗,因為效能就是使用者體驗。
為此,我們設計完成了一種全新的,效能優先的 Web 端體驗。當我們決定首先使用哪些頁面做嘗試時,我們選擇了產品列表頁面和主頁,因為 Google Analytics 的統計表明這些頁面的自然流量最大。
我們怎麼做到的
從現實世界中的效能預算開始
我們做的第一件事就是起草效能預算,以避免犯下未經檢查的臃腫問題(我們之前的 Web 應用中的一個問題)。
效能預算讓每個人都在同一個“頁面”上。它們有助於創造一種共享熱情的文化,以改善使用者體驗。具有預算的團隊還可以更輕鬆地跟蹤和繪製進度。這有助於支援那些擁有有意義的指標的執行發起人,指明正在進行的投入的合理性。
由於 在載入過程中存在多個時刻,都會影響到使用者對這個頁面是否“足夠快”的感知 ,我們將預算基於一套組合的指標。
載入網頁就像一個有三個關鍵時刻的電影膠片。三個時刻分別是:它發生了嗎?它有用嗎?然後,它能用起來嗎?
我們決定為關鍵路徑的資源設定 120 KB 的上限,在所有頁面上還有一個 2 秒的 首屏內容渲染 和 5 秒的 可互動時間 限制。這些數字和指標都是基於 Alex Russell 的一篇發人深省的文章 真實世界的 Web 效能預算 以及 Google [以使用者為中心的效能指標]。
關鍵路徑資源120KB 首屏內容渲染2s 可互動時間5s Lighthouse 效能得分> 85 複製程式碼
:arrow_up_small: 我們的效能預算 :star2:
為了能把效能預算堅持下去,我們在一開始選擇庫時就十分慎重,包括 react、react-router、redux、redux-saga 和 unfetch 。
我們還整合了 bundlesize 到我們的 PR 流程當中,用來執行我們在關鍵路徑資源上的效能預算方案。

:warning: bundlesize 阻止了一個超出預算的 PR :no_entry_sign:
理想情況下,我們也會自動檢查 首屏渲染時間 和 可互動時間 指標。但是,我們目前還沒有這樣做,因為我們想先發布初始頁面。我們認為我們可以通過我們的小團隊規模來避免這種情況,每週通過我們的 Lighthouse 稽核我們的釋出,以確保我們的變更在預算範圍內。
在我們積壓的工作中,下一步就是自建效能監控框架。
我們如何讓它(看起來)變快了
-
我們採用了一部分 PRPL 模式 。 我們為每個頁面請求傳送最少量的資源(使用 基於路由的程式碼拆分 ),並 使用 Workbox 預先快取應用程式包的其餘部分 。我們還拆分了不必要的元件。例如,如果使用者已登入,則應用程式將不會載入登入和註冊元件。目前,我們仍然在幾個方面偏離了 PRPL 模式。首先,由於我們沒有時間重新設計的舊頁面,該應用程式有多個應用程式外殼。其次,我們還沒有探索為不同的瀏覽器生成單獨的構建打包。
-
內聯的關鍵的 CSS。 我們使用 webpack 的 mini-css-extract-plugin 來提取並內聯的方式引入對應頁面的關鍵 CSS,以優化首屏渲染時間。這樣就給使用者提供了 一些事情 正在發生 的感覺。
-
懶載入視口外的影象。並且逐步載入它們。我們建立了一個滾動觀察元件,其基於 react-lazyload ,它會監聽滾動事件,一旦計算出影象在視口內,就開始載入影象。
-
壓縮所有的影象來減少在網路中傳輸的資料量。這將在我們的 CDN 提供商的自動化影象壓縮 服務中進行。如果你不使用 CDN,或者只是對影象的效能問題感到好奇,Addy Osmani 有一個 關於如何自動進行影象優化的指南 。
-
使用 Service Worker 來快取網路請求。這減少了資料不會經常變化的 API 的資料使用量,並改善了應用程式後續的訪問載入時間。我們找到了The Offline Cookbook 來幫助我們決定採用哪種快取策略。直到我們有了多了應用外殼,Workbox 預設的
registerNavigationRoute
並不適用於我們的實際場景,所以我們不得補自行完成一個 handler 來匹配當前應用外殼的導航請求。
workbox.navigationPreload.enable(); // From https://hacks.mozilla.org/2016/10/offline-strategies-come-to-the-service-worker-cookbook/. function fetchWithTimeout(request, timeoutSeconds) { return new Promise((resolve, reject) => { const timeoutID = setTimeout(reject, timeoutSeconds * 1000); fetch(request).then(response => { clearTimeout(timeoutID); resolve(response); }, reject); }); } const networkTimeoutSeconds = 3; const routes = [ { name: "collection", path: "/categories/.*/?$" }, { name: "home", path: "/$" }, { name: "listing", path: "/p/.*\\d+/?$" }, { name: "listingComments", path: "/p/.*\\d+/comments/?$" }, { name: "listingPhotos", path: "/p/.*\\d+/photos/?$" }, ]; for (const route of routes) { workbox.routing.registerRoute( new workbox.routing.NavigationRoute( ({ event }) => { return caches.open("app-shells").then(cache => { return cache.match(route.name).then(response => { return (response ? fetchWithTimeout(event.request, networkTimeoutSeconds) : fetch(event.request) ) .then(networkResponse => { cache.put(route.name, networkResponse.clone()); return networkResponse; }) .catch(error => { return response; }); }); }); }, { whitelist: [new RegExp(route.path)], }, ), ); } 複製程式碼
⚙️ 我們對所有的應用外殼採用了一個超時時間為 3 秒的網路優先策略 :shell:
在這些變化中,我們嚴重依賴 Chrome 的“中端移動裝置”模擬功能(即網路限制為 3G 速度),並建立了多個 Lighthouse 審計來評估我們工作的影響。
結果:我們怎麼做到的

:tada: 比較之前和之後的移動 Web 指標 :tada:
我們新的 PWA 列表頁面的載入速度比我們舊的列表頁面 快 3 倍 。在釋出這一新頁面之後,我們的印度尼西亞的自然流量與我們所有長時間的周相比,增長了 63%。在 3 周的時間內,我們還看到,廣告點選率 增加了 3 倍 ,在列表頁面上發起聊天的匿名使用者 增加了 46% 。

⏮ 在較快的 3G 網路下的 Nexus 5 上,我們列表頁面的前後對比 。更新: WebPageTest 對這個頁面的簡單報告 。 ⏭
快速,自信地迭代
一致的 Carousell 設計系統
在我們開展這項工作的同時,我們的設計團隊也在同時建立標準化設計系統。由於我們的 PWA 是一個新專案,我們有機會根據設計系統建立一組標準化的 UI 元件和 CSS 常量。
擁有一致的設計使我們能夠快速迭代。每個 UI 元件 我們只構建一次,然後在多個地方複用它 。例如,我們有一個 ListingCardList
元件,它顯示列表卡片的提要並觸發回撥,以便在滾動到結尾時提示其父元件載入更多列表。我們在主頁,列表頁面,搜尋頁面和個人資訊頁面中使用了它。
我們還與設計師合作,一起確定應用程式設計中的適當性能權衡。這使我們能夠維持我們的效能預算,改變一些舊設計以符合新設計,並且,如果它們太昂貴了的話,就放棄花哨的動畫。
與 Flow 同行
我們選擇將Flow 型別定義作為我們所有檔案的必選項,因為我們想減少煩人的空值或型別問題(我也是漸進型別的忠實粉絲,但為什麼我們選擇了 Flow 而不是TypeScript 就是下一次的一個話題了)。
在我們開發和建立了更多程式碼時,採用了 Flow 的選擇被證明非常有用。它讓我們有信心新增或更改程式碼,將核心程式碼重構得更加簡單和安全。這使我們能夠快速迭代而不會破壞事物。
此外,Flow 型別也對我們的 API 約定和共享庫元件的文件非常有用。
對於強制將 Redux 操作和 React 元件的型別寫出來這件事情,還有一個額外的好處,就是它會幫助我們仔細思考如何設計我們的 API。它也提供了與團隊開始早期的 PR 討論的簡單途徑。
小結
我們建立了一個輕量級的 PWA 來為我們具有不可靠網速的使用者提供服務,一個頁面接一個頁面地釋出,提高了我們的商業指標 和 使用者體驗。
是什麼幫助我們保持足夠快的速度
- 擁有並堅持一份效能預算
- 降低關鍵渲染路徑到最小
- 經常使用 Lighthouse 進行審計
是什麼幫助我們快速迭代
- 擁有標準化的設計系統及其相應的 UI 元件庫
- 擁有完全型別化的程式碼庫
結束思考
回顧過去兩個季度我們所做的事情,我們為我們新的移動 Web 業務體驗感到無比自豪,我們正在努力使其變得更好。這是我們第一個專注於速度的平臺,也更多的思考了一個頁面的載入過程。我們的 PWA 對業務和使用者指標的改進有助於說服公司內部更多人去了解應用程式效能和載入時間的重要性。
我們希望本文能夠啟發您在設計和構建 Web 體驗時考慮效能。
在此為參與這個專案的人歡呼:Trong Nhan Bui、Hui Yi Chia、Diona Lin、Yi Jun Tao 和 Marvin Chin。當然也要感謝 Google,特別是要感謝 Swetha and Minh 對這個專案的建議。
感謝 Bui、Danielle Joy、 Hui Yi 、 Jingwen Chen 、See Yishu 和Yao Hui Chua 的寫作和校對。
最後,多虧了Hui Yi、 Yao Hui Chua 、 Danielle Joy 、Jingwen Chen 和See Yishu。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為掘金 上的英文分享文章。內容覆蓋 Android 、 iOS 、 前端 、 後端 、 區塊鏈 、 產品 、 設計 、 人工智慧 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃 、官方微博、 知乎專欄 。