Dropbox 瘦身攻略:我們如何把JavaScript包縮小三分之一

2023-09-21     InfoQ

原標題:Dropbox 瘦身攻略:我們如何把JavaScript包縮小三分之一

作者 | Umair Nadeem,Rich Hong

譯者 | 核子可樂

策劃 | 丁曉昀

不知道各位朋友是否還記得,上一次正打算點擊網站上的按鈕、結果頁面突然變化導致你點上了錯誤的位置是什麼時候。或者說,上一次你因為實在忍受不了緩慢的加載速度而憤然點叉又是在什麼時候?

這些問題在如今內容愈發豐富、交互度越來越高的應用場景中被無限放大。為了支持更複雜的功能,我們不得不編寫出更多的前端代碼,導致瀏覽器端需要接收、解析和執行的位元組更多,最終性能自然變得更差。

在 Dropbox,我們深深了解此等糟糕體驗是多麼令人崩潰。所以過去一年來,我們的 Web 性能工程團隊抽絲剝繭、將性能問題溯源到了一個常常被忽視的元素身上:模塊捆綁器。

米勒定律認為,人腦在任何給定的時間內只能容納一定量的信息,所以大部分現代代碼庫(包括我們 Dropbox 的代碼庫)才會被拆分成一個個更小的模塊。模塊捆綁器負責把應用程式中的各類組件(例如 Java 和 CSS)合併成捆綁包,並在頁面加載時由瀏覽器下載這些捆綁包。最常見的處理方式就是將捆綁包保存為最小 Java 文件的形式,用以存放 Web 應用程式中的大部分邏輯。

Dropbox 模塊捆綁器的首次疊代設計於 2014 年,當時以性能為先的模塊捆綁方法才剛剛興起(分別在 2012 年和 2015 年由 Webpack 和 Rollup 率先提出)。但畢竟年代久遠,那時候的方案跟現代設計比起來還是太過簡陋。我們的模塊捆綁器並沒多少性能優化,使用起來比較繁瑣,既影響用戶體驗又會拖慢開發速度。

隨著捆綁器逐漸顯露老態,我們決定面向未來做好性能優化、全面替換掉這位應當功成身退的老將。當前也是替換的最佳時機,因為我們正好在著手將頁面遷移至 Edison(我們的全新 Web 服務棧),統籌規劃有望一箭雙鵰。替換之後,我們的靜態資產管線也將迎來更現代的捆綁器,在架構層面讓集成更為簡單。

現有架構

雖然我們原本的捆綁器擁有相對較快的構建速度,但也存在著不少短板,包括捆綁包太過臃腫、工程師們感到難以維護等等。工程師們只能手動定義把哪些腳本跟包捆綁在一起,而且我們之前只簡單提供頁面渲染所需要的包,但幾乎未做任何性能優化。隨著時間推薦,這種粗糙的方案也帶來了以下幾大顯著問題。

問題一:捆綁代碼有好幾個版本

直到不久前,我們還在使用名為 Dropbox Web Server(DWS)的自定義 Web 架構。簡單來講,每個頁面都由多個小頁(pagelet,即頁面中的子部分)組成,因此導致每個頁面都有多個 JS 入口點,而各 servlet 也由後端處對應的控制器提供服務。雖然這種部署在多個團隊同時處理同一頁面時速度更快,但也往往導致 pagelet 指向不同的後端代碼版本。這就要求 DWS 能支持在同一頁面上交付不同版本的打包代碼,而這經常會引發一致性問題(例如在同一頁面上加載相同單例的多個實例)。我們向 Edison 的遷移將消除這種 pagelet 架構,從而更靈活地採取更符合行業標準的捆綁方案。

問題二:需要手動分割代碼

所謂代碼分割,就是把 JS 包分割成更小的塊的過程,這樣瀏覽器就能只加載當前頁面所需要的代碼庫部分。例如,假設用戶先訪問 dropbox.com/home,而後訪問 dropbox.com/recent,那麼如果不進行代碼分割,則瀏覽器會下載整個 bundle.js,這無疑將顯著減慢頁面的初始導航速度。

所有頁面的全部代碼均通過單一文件提供

但在代碼分割之後,瀏覽器只需要下載頁面所需要的各個代碼塊。由於瀏覽器下載的代碼量更少,所以 dropbox.omc/home 的初始導航速度將大大提升。此外,代碼分割可以保證先加載關鍵腳本,而後再異步加載、解析和執行非關鍵腳本。共享代碼片段也將被瀏覽器緩存下來,進一步減少用戶在不同頁面間移動時所需下載的 JS 代碼量。所有這些,都將大大減少 Web 應用程式的加載時間。

僅下載頁面所需的新代碼塊

由於我們現在的捆綁器沒有任何內置的代碼分割工具,所以工程師只能手動對包做定義。具體來講,我們的打包 map 是個 6000 多行的龐大字典,具體指定了哪些模塊該放進哪個包中。

可以想見,隨著時間推移這樣一套架構的維護工作將變得異常複雜。為了避免非優打包,我們強制執行了一套嚴格的測試(打包測試),但因為每次變更都可能打亂原本的模塊排列,所以工程師們變得神經緊張、苦不堪言。

這也導致我們的實際代碼量比頁面所需要多得多。例如,假定我們有以下包 map:

如果頁面依賴於模塊 a、b 和 c,則瀏覽器只須進行兩次 HTTP 調用(分別獲取 pkg-a 和 pkg-b),而非對各模塊各進行一次(共三次)調用。這雖然會減少 HTTP 調用的開銷,但同時也會加載不必要的模塊——在本示例中就是模塊 d。由於缺乏搖樹優化,我們不但加載了不必要的代碼,還加載了頁面不需要的整個模塊,因此會拖慢整體用戶體驗。

問題三:缺少搖樹優化

搖樹是一種包優化技術,能夠消除未使用的代碼來幫助捆綁包瘦身。假設我們的應用程式需要導入包含多個模塊的第三方庫,如果沒有搖樹優化,則實際加載的大部分捆綁代碼其實都毫無用處。

無論是否實際使用,所有代碼都會被捆綁進來

通過搖樹優化,我們可以分析代碼的靜態結構,並刪除一切未被其他代碼直接引用的代碼。這樣最終的捆綁包就能更加精簡小巧。

只捆綁要使用的代碼

因為我們之前的捆綁器不太完善,所以其中沒有任何搖樹功能。生成的包往往包含大量未使用代碼,特別是來自第三方庫的代碼,這會導致頁面加載無用內容、延長等待時間。此外,因為我們使用 protobuf 定義來實現從前端到後端的高效數據傳輸,所以在檢測某些可觀察性指標時往往要引入高達幾 MB 的未使用代碼!

為何選擇 Rollup

多年來我們其實考慮過不少解決方案,並最終把核心需求梳理了出來:我們真正需要的,就只有自動代碼分割、搖樹優化,以及可以進一步優化捆綁管線的可選插件。Rollup 就是當前最成熟、也能靈活融入到我們現有構建管線的工具,於是最終成為我們的首選解決方案。

另一個原因是:有助於降低工程開銷。因為我們已經在使用 Rollup 捆綁我們的 NPM 模塊,所以繼續擴大 Rollup 的使用範疇肯定比再引入新工具要划算得多。此外,這也意味著跟其他捆綁器相比,我們已經在之前的運營中掌握了更多關於 Rollup 特性的工程專業知識,能有效降低用不下去的可能性。最後,我們還算了一筆帳,發現跟深入集成 Rollup 相比,在原有模塊捆綁器中重現 Rollup 的功能需要投入更多工程資源。

不負眾望的 Rollup

我們都知道,安全、分步推出模塊捆綁器絕非易事,畢竟我們在期間需要同時可靠支持兩種模塊捆綁器(並生成兩種對應的捆綁包)。我們主要關心的問題包括如何保證捆綁代碼穩定、無 bug,如何增加構建系統和 CI 的負載,還有怎樣激勵團隊接受在其頁面中使用 Rollup 捆綁包。

考慮到可靠性和可擴展性等問題,我們把發布過程分成了四個階段。

  • 開發者預覽階段:允許工程師在開發環境中選擇加入 Rollup 捆綁包。這樣我們就能讓開發者儘早發現 Rollup 捆綁包引發的任何意外,藉此推動行之有效的眾包 QA 測試。認真收集相關信息後,我們將有充足的時間解決 bug、適應範圍變更。
  • 面向 Dropbox 員工的內部預覽階段將全面推廣 Rollup 捆綁包,藉此收集早期性能數據並進一步獲取關於應用程式行為變化的實踐反饋。
  • 通用階段,即逐步向所有 Dropbox 用戶(包括內部和外部用戶)推出 Rollup 捆綁包。在此之前,我們已經對 Rollup 包做過徹底測試並確定其穩定性已經達到較高水平。
  • 維護階段,強調解決項目中遺留的所有技術債,再通過疊代讓 Rollup 進一步優化性能和開發者體驗。我們意識到,如此規模的大體量項目將不可避免地積累下一些技術債,我們應計劃在某個階段將其解決,而不能假裝債務不存在。

為了有效支持各個階段,我們混合使用了基於 cookie 的門控和內部功能門控系統。以往,Dropbox 的大多數部署都純粹藉助我們的內部功能門控系統得以完成。但這一次,我們決定允許基於 cookie 的門控在 Rollup 和舊捆綁包間快速切換,從而加快調試速度。每個發布階段都以交替形式分步推出,包括從 1%、10%、25%,到 50% 乃至最終的 100%。這讓我們能夠靈活地收集早期性能與穩定性結果,當發現問題時進行無縫回滾,同時儘可能降低對內、外部用戶造成的影響。

因為我們需要遷移大量頁面,所以除了建立安全可靠的 Rollup 切換策略之外,還得激勵頁面所有者主動執行切換。由於我們的 Web 棧將配合 Edison 進行一波重大改造,所以這應該是個可以一箭雙鵰的絕佳時機。如果把 Rollup 塑造成 Edison 所支持的獨特功能,那開發團隊應該會更願意同時接受 Rollup 和 Edison,我們也能藉此將 Rollup 的遷移策略跟 Edison 升級緊密綁定起來。

Edison 也有望藉此提高自己的性能和開發速度。我們認為,將 Edison 與 Rollup 相結合,會在整個公司內產生強烈的轉型協同效應。

挑戰與障礙

我們早就做好了迎接意外挑戰的準備,但事實證明將一種構建系統(Rollup)跟另一種構建系統(基於 Bazel 的原有基礎設施)進行複雜對接,其挑戰性要遠遠大於我們的任何想像。

首先,我們發現同時運行兩種不同模塊捆綁器,所消耗的資源要遠超我們的估計。Rollup 的搖樹算法雖然相當成熟,但仍需要將所有模塊都先加載到內存中,之後生成分析關係並搖出代碼所需的抽象語法樹。此外,我們將 Rollup 集成到 Bazel 中的作法,限制了我們緩存中間構建結果的能力。也就是說,我們需要持續集成以重建並重新縮小每個構建上的全部 Rollup 塊。這導致我們的持續集成構建因內存耗盡而超時,顯著拖慢了部署節奏。

我們還發現了 Rollup 搖樹算法中的幾個 bug,這會導致搖樹優化過於激進。值得慶幸的是問題不大,我們在開發者預覽階段就將其修復,所以最終用戶並未受到影響。此外,我們發現舊版捆綁程序會提供來自第三方庫的某些代碼,而這些代碼與 JS 嚴格模式並不兼容。一旦將這些代碼提交給採用嚴格模式的新捆綁器,則會在瀏覽器中引發極為嚴重的運行時錯誤。這就要求我們對整個代碼庫、特別是與嚴格模式不兼容的補丁代碼,開展一輪全面審計。

最後,在 Dropbox 內部員工預覽階段,我們發現 Rollup 和舊版捆綁器之間的 A/B 遙測指標並未體現出符合預期的 TTVC 性能提升。我們最終意識到,這是因為 Rollup 生成的代碼塊比舊版捆綁器生成的代碼塊要多得多。儘管我們最初假設 HTTP2 的多路復用能消除大量代碼塊引發的性能下降,但事實證明代碼塊過多還是會導致瀏覽器耗費更長的時間來獲取頁面所需的各模塊。再有,模塊數量的增加也會拉低壓縮效率,因為 Zlib 等壓縮算法使用的是滑動窗口方法執行壓縮,就是說單一大文件的壓縮效率要明顯好於多個小文件。

最終結果

在向全體 Dropbox 用戶推出 Rollup 之後,我們發現新項目將 Java 包縮小了約三分之一,JS 腳本總量減少了 15%,TTVC 也實現了適度改進。我們還通過自動代碼分割顯著提高了前端開發速度,開發人員現在不必在每次變量時都手動調整捆綁包定義。最後,也可能是最重要的一點在於,我們完成了捆綁基礎設施的現代化改造,削減了自 2014 年以來積累的大量技術債,顯著減輕了未來的項目維護負擔。

除了令人眼前一亮的實踐表現之外,Rollup 項目還幫助我們發現了現有架構中的幾個瓶頸:例如多個渲染會阻塞 RPC,對第三方庫的函數調用過多,以及瀏覽器加載模塊依賴性 map 效率太低等。憑藉 Rollup 豐富的插件生態系統,解決原有代碼庫中此類瓶頸正變得越來越簡單。

總而言之,全面採用 Rollup 作為模塊捆綁器不僅給性能和生產力帶來立竿見影的提升,也將在未來幫助 Dropbox 實現更為顯著的性能改進。

原文連結

https://dropbox-tech.translate.goog/frontend/how-we-reduced-the-size-of-our-java-bundles-by-33-percent

相關閱讀

跨過四個時代,Java 框架終於可以與原生應用 SDK 競爭了

最佳的 18 個 JAVA 前端開發框架和庫 (https://xie.infoq.cn/article/e976363d22c7ffd126d9b6eb1)

新一波 Java Web 框架 (https://www.infoq.cn/article/2SyNfw6RkyTV4gkRavIQ)

Java 框架大戰已結束,贏家只有一個 (https://www.infoq.cn/article/GDc7cryCCPOhQS9FuAKh)

聲明:本文為 InfoQ 翻譯,未經許可禁止轉載。

點擊底部閱讀原文訪問 InfoQ 官網,獲取更多精彩內容!

今日好文推薦

當我想要構建一款 LLM 應用時:關於技術棧、省錢和遊戲規則

耗時一年用戶從 0 增長至 1400 萬,背後僅三名工程師,這家社交巨頭背後的技術棧是如何搭建的?

行業老兵聊 To B 產品技術:To B 難,難不過做好軟體

網易回應員工因 BUG 被 HR 威脅後輕生;阿里新 CEO:要讓 85、90 後成為主力管理者;華為 Mate60 正面「剛贏」蘋果?| Q 資訊

文章來源: https://twgreatdaily.com/zh-cn/057a91872380422b9aff64c3815cb9e6.html