嗶哩嗶哩 Web 首頁重構——回首2021

陶然陶然發表於2022-12-07

   01 前言

  在 2021 年時我們透過資料分析發現:在電腦端有越來越多使用者的電腦螢幕切換成了大屏,現有 B 站的網頁設計風格已經難以在寬屏裝置上高效率的做內容分發,因此我們決定對 B 站網頁版的整體視覺風格做一個大的更新,目標是能夠在大屏和小屏上都能夠對內容做到高效率的分發。

  B站的網頁端頁面數量眾多、業務邏輯複雜,因此我們決定先從使用者感知最明顯、且相對複雜的B站首頁開始推進這個計劃。

   02 方案

  使用者:

  因為B站的使用者大多是年輕人,所以對於改版的接受能力較強,且使用者軟體版本較新,因此我們可以採用稍微激進一些的技術方案配合灰度控制來做改版。

  部分小眾使用者有自定義B站主題的需求,因此在改版前要考慮他們 hack 時的體驗。

  產品:

  以提高分發效率為目標,嘗試提升內容屏佔比 + 更深入的與 AI 推薦相結合做內容分發。

  設計:

  為了讓整個網頁保留適配窄屏(iPad)和更大螢幕(帶魚屏)的可能性,整個新版網頁的設計語言是“柵格化 + 響應式”。

  採用更扁平化的結構設計,弱化色彩,增強對內容的呈現;結構化頁面元素,元件化設計,提升產品迭代效率。

  研發:

  JS:在 2021 年這個時期對 Web 專案進行大規模重構較好的技術選型是 NextJS(React)或 Vue3,在公司內部我們 Vue 用的較多,且 NextJS 更適合海外專案,因此我們決定使用 Vue3 作為基礎框架。當時基於 Vue3 的 SSR 方案還在演化,ESR 的方案少有用例,再加上團隊在過往的積累中對 SSR 更加熟練,因此我們決定基於 Vue3 框架自己實現一套 SSR 渲染的方案。(PS:體驗上 CSR < SSR < ESR)

  CSS:在做響應式和柵格化的技術方案選型時,可使用百分比寬度、calc() 計算寬度、JS 計算寬度等;在佈局方案上,可選的有 flex、Grid 佈局等;CSS 框架上可選的有 Sass、Less 等,最終我們為了實現一個更完美的頁面佈局,採用了@media + grid + Sass 的方案。(PS:Sass 使用率較高、Grid 是二維柵格佈局、@media 是響應式必備)

  多主題:在多主題支援上可選的方案有:在預編譯階做變數收集、DesignToken、CSS-var,最終我們選擇了 CSS-var 的方案,是因為:預編譯階段的變數不利於透過瀏覽器外掛注入變數的形式覆蓋主題色,DesignToken 的形式過於靈活反而導致工程複雜度會變高和效能變差,而 CSS-var 在現階段的可用性相對來說是較好的。

  元件化:元件化方案我們採用了 monorepo/Lerna 來管理倉庫,把專案拆分成了 PureJS dependencies → Framework(UI) dependencies → Service dependencies → Project/Page,具象化表達就是指:http.ts → VideoCard.vue → BiliHeader.umd.js → Homepage.vue

  總結:最終我們的技術方案是:Lerna + Vite + Vue3(SSR、ts、setup)+ Grid + @media + Sass + CSS-var

   03 難點

  在整個專案的開發過程中從工程層面去解構重難點,其中值得與大家分享的點有:

  如何抽象頁面的柵格化/響應式,做到可複用、可拆解

  如何以最低成本實現一套 Vue3 SSR/CSR 同構方案,支援頁面降級

  如何實現一套 API → Store → View(List → Item) 的高可複用模式

  如何讓網頁兼顧使用者體驗、可維護性、效能

  接下來我們針對上述問題逐一做解答。

  3.1 如何抽象頁面的柵格化/響應式

  做到可複用、可拆解  

  得益於我們使用 Grid + @media 來做響應式和柵格化這裡不存在特殊的難點,難點在於下面這個場景:  

  產品要求:在不同螢幕尺寸下導航欄區域展示的分割槽個數不同,且要求永遠只把最後幾項收錄到“更多”裡。

  難點分析:因為首頁是 SSR 的且導航欄在第一屏,所以我們需要在頁面渲染時就展示出正確的資料,那麼它必定是使用 CSS 實現的邏輯而非 JS 去計算。那麼我們要實現的就是:透過 CSS 在不同螢幕尺寸下把一個列表的最後幾項隱藏或展示。我們的實現方案是:  

  核心的程式碼邏輯就是藉助 CSS 的 nth-of-type 選擇器 + Sass mixin 來實現,我們對該邏輯進一步封裝,得到:  

  最終我們在導航欄這裡的佈局程式碼是:  

  為了能夠更加靈活的適配各個頁面的佈局場景,因此我們對 Sass mixin 的拆分比較原子化,在程式碼中就會出現有抽象程度較高的 @include 呼叫,但整體上只要對 Grid 佈局和 Sass 語法足夠熟練就快速掌握,提升開發效率。

  3.2 如何以最低成本實現一套

  Vue3 SSR/CSR 同構方案支援

  頁面降級

  在 Vue2 框架裡主流的 SSR 實現方案是藉助 Vuex,主要流程是呼叫 store.replaceState() 函式進行 hydrate(store.replaceState 把 window 下序列化後的資料再反序列化,重新迴歸到 store 中;頁面/元件依賴的資料從 store 中獲取,以此完成 hydrate)。

  所以這裡的核心點在於:我們只需要在 client 讓 Vue 框架的資料從本地(window)獲取而非服務端獲取即可,在常見的技術語境中我們稱這個過程為 —— 重放。我們在服務端獲取到的資料儲存在 window 下,當客戶端再次初始化的時候會重新發起請求,這個時候的請求結果從本地獲取即可。因此我們要做的就是:封裝出一個 http.ts 的 PureJS dependencies,讓它能夠實現同構(重放)。

  同時,因為 http.ts 是支援同構的,所以哪怕 SSR 服務掛了直接降級,頁面也可以以 CSR 的方式正常渲染。

  3.3 如何實現一套 API → Store

  → View(List→Item)的高可復

  用模式

  B站的首頁有眾多樓層,一些樓層雖然從視覺上差異不大,但是業務方卻不同,因此會涉及到資料格式規範化的問題。

  在常規的架構中,我們會在前端程式碼裡抽象出 API 層來實現資料快取、資料轉換、分頁控制等,然後把資料匯入 Store 中再分發到頁面和元件裡。但是 B站首頁的特點在於:各個模組之間的資料基本沒有互動,因此並不需要一箇中心化的 Store 來聚合狀態,我們要做的模型是 API → Component(List) → Component(Item),在 API 層處理好資料差異的前提下,只要 List 抽象的足夠好,Item 的編寫也會很簡單,達到模組化、可組合的效果。

  這裡的實現不會過多的著墨,可以參考:這個庫的實現[1]。

  3.4 如何兼顧使用者體驗、

  可維護性、效能

  在上文中我們講到了自己實現 Vue SSR 的邏輯,熟練 Vue SSR 的讀者一定對 asyncData 這個函式非常熟悉,它在 Vue2 裡面只能應用於 page-component,導致專案的可維護性變低(參考:如何評價 React Server Component.)  

  本質原因是:如果允許非頁面元件內呼叫 asyncData 就會導致整個網路請求是序列的,而放在 page-component 去統一呼叫那就是並行的。React 的解決方案是 React Server Component,我們在使用 Vue3 重構B站首頁時採用了一種另類的方法 —— 服務端重放。在專案中,每個元件所需的資料仍然在元件中去發起請求,在頁面首次渲染時請求會是序列模式,然後我們會在 http.ts 裡面把當前頁面的 Request 收集起來,在下一次該頁面再訪問的時候,重放這些 Request 即可獲得最新的 Response。

  傳統的快取策略是快取 Response,這樣會導致在千人千面場景下遇到資料不可複用的問題和資料時效性問題,但當我們去儲存 Request 時,在絕大多數場景下都是能夠順利工作的。

  解決完一些技術難題後,我們的頁面順利的搭建起來了,那麼最後它的效能是什麼表現呢:  

  第一次跑分的時候只有22分,也就是說我們還有70+分的最佳化空間,接下來我就給大家分享以下 PC 新首頁的最佳化思路。

  透過 Vue3 SSR 的同構 hydrate 和 http.ts 的重放方案,我們已經解決了可維護性和使用者體驗的問題,接下來我們主要去解決效能問題。

  3.5 效能最佳化原則

  1.能夠使用 CSS 解決的問題就不要使用 JS;

  2.需要掌握的知識有:學會使用 chrome devtools 的 performance 皮膚,檢視火焰圖;瞭解 reflow、repaint;

  3.效能最佳化場景:首屏、靜置、互動;

  4.效能最佳化原則:  

  3.6 效能最佳化實踐

  1.使用 CSS 實現特殊文字頂格效果(能使用 CSS 就不用 JS)  

  

  2.使用 CSS 代替 Lottie 動畫繪製輪播圖特效(靜置場景的最佳化)  

  3.使用 setTimeout + requestIdelCallback 載入首屏內嵌的 iframe 頁面(首屏場景 + 最佳化執行順序)  

  4.外部 SDK 延遲載入(script defer),並在呼叫時 retry(首屏場景 + 非同步化)  

  5.在頁面滾動時使用 IntersectionObserver 代替 getBoundingClientRect 判斷內容與視口的交叉關係(互動場景 + 避免 reflow)

  6.能單例化/可複用的場景就適當複用 + 延遲銷燬(首頁的 Popover 元件)

  7.針對特定指標的最佳化,如:為了避免發生 CLS 我們給所有的元件都加了相同尺寸的骨架屏

  以上提到的只是整個最佳化過程中的案例,基於效能最佳化的原則然後秉著“哪怕芝麻也是肉”的心態去一步一步的最佳化,最終首頁的效能指標是:  

  (PS:LCP 無法最佳化是因為首屏的一些圖片對清晰度的要求較高,所以沒有對它做進一步的最佳化)

   04 結語

  嗶哩嗶哩 PC Web 在2021年年初的時間點使用了較為激進的技術改造方案,一方面是出於對效能的考量,另一方面是不想為後人留下“技術債”。雖然整個開發過程中要克服很多困難(開源生態不完美、優秀實踐沒參考),但整體上還是達到了我自己非常滿意的一個水準。在工程化、元件化、效能上都相較於以往的程式碼有一定的進步,也為後續其它頁面的改造留下了一些實踐參考,這個過程中離不開團隊的努力和上級對我們技術選型方案的支援,這是一段非常不錯的 coding 經歷。

  站在2022年末的當下來回顧當初的技術選型,B站在 PC Web 端重構時表現出來的技術水平(技術選型的先進性 + 解決方案的先進性 + 執行結果的高效性)在國內各大網際網路 ToC 業務場景下也是領先的(常態下效能:Vue2 < React < Vue3,目前國內各大影片垂類網際網路公司,只有B站在 ToC 主要業務場景使用了 Vue3,資料來源:框架採用分佈,框架效能對比)。

來自 “ 嗶哩嗶哩技術 ”, 原文作者:劉磊;原文連結:http://server.it168.com/a2022/1207/6779/000006779080.shtml,如有侵權,請聯絡管理員刪除。

相關文章