B站新版播放頁優化總結報告
前言
頁面從輸入網址到頁面加載出第一屏的內容,我們稱之為首屏資料,從白屏到頁面渲染完成這個過程是有時間消耗的,大多數情況下我們也是壓縮這段時間來做首屏的效能,時間越短 頁面呈現的越快 使用者感受到的體驗也會越好。一般網頁都會是這樣去做的,但播放頁的“首屏”略有不同,播放頁是以視訊為主,頁面的核心是視訊的畫面,所以我們會重新定義播放頁的“首屏”為視訊的首幀(播放器拉取視訊流到可播放時間,大概在200KB左右)可想而知 這個定義下的“首屏”參考要比一般網頁的首屏的要嚴格,不僅僅是視覺上的可見,還有互動上的可播(播放按鈕可以點選)。
發個圖先
截止十月初

image.png
上圖為2018年1月份~10月份的播放頁首幀資料優化圖(最終指標參考藍色線條)。從圖上可以看到有意思的一點,在優化過程中資料忽高忽低,但整體是下降趨勢,最終還是下降到1.5s一下,完成Q3優化任務。這個也反映了 我們在做頁面優化的時候不一定所有的想法都會是正確的,也是通過不斷的嘗試、試驗,最終留下可行方案。
網上有很多網頁效能優化方案一搜一大堆,一些基礎常用的優化我就不在這裡贅述了,下面說的是我們覺得收益不錯且有針對性的一些優化。
首屏直出
雖然前後端分離給開發體驗和職責分離上來了很多的好處,但是在頁面渲染和使用者體驗上卻有著天然的劣勢,模板和資料分開處理了,請求頁面得到的只是一個HTML容器,還要非同步去請求介面,資料成功返回了才能做處理,硬是吧一個同步的邏輯變成了非同步,最後還要走一遍業務邏輯+生成DOM然後插入到容器裡渲染,整個過程會有長時間的白屏等待時間,體驗很差。
為了第一時間讓頁面上有東西呈現出來,優化方案裡首屏直出一定是第一要做的,直接輸出HTML和資料( INITIAL_STATE ) 讓頁面同步渲染,極致的做法可以只輸出首屏需要的DOM和CSS,這個時候整個頁面的骨架等都已經出來,使用者體驗上省去了等待白屏時間或旋轉不停的Loading圖。
在Node介入之前,播放器頁面的元件也是基於vue來做的,所以在選型上用的是Node+Vuejs+Memcached(MC)這樣一套組合來實現的SSR,完成了首屏直出需求的同時也實現了前後端同構的目的。也正是有了這些能力,後面我們很多的優化都基於node服務端來做的。(這裡主要講頁面優化,後面如果有機會在新起一篇單獨說下SSR和快取之類的)
請求合併 特殊的頁面特殊處理
先看下播放頁的頁面資料組成部分(除播放器相關)
介面:稿件資訊(view) 、推薦列表(related)、up主資訊(card)、視訊標籤(tag)、熱門評論(comment)
根據業務需要播放頁承擔了很大的SEO的職責,所以上面的介面資訊基本是要在模板裡面帶出來的(給爬蟲) 那麼這時候就有個很明顯的問題,多個介面的情況下而且介面之間可能還有依賴關係,很難保證這些介面返回的效率,如果返回的太慢 QPS一定會很低,所以聚合介面也是我們的首選,但又擔心一個問題是後端會幫我們去合併介面嗎? 雖然我們的後端同學都很好溝通 後面才覺得 對於公司的重點核心專案來說,做的再多也不為過,因為一切的努力,受益的是使用者。 很順利五個介面合五為一,由於後端走的是內網請求且做了一些快取優化,聚合後的介面也是非常的快,前端node拿到介面大概控制在20ms以內。
跳出框架
vuejs是個好東西,提升開發效率同時、又是面向元件化模組化開發、資料驅動、生命週期等,給開發帶來很多的便利。這麼完美的一個框架,我實在想不出或不願意去想它有哪些“劣勢”。直到我們在給頁面做優化的時候,你就會發現它原先的一些優勢會在這個問題(極致優化)下可能是一個劣勢。
簡單的列一下播放頁中碰到的“劣勢”:
一、在做服務端渲染(ssr)的時候,我們在渲染元件的時候發現是在太弱了,與常見的服務端模板(ejs,pug...)簡直不能比 (如圖:壓測)

image.png
我們先對node單點做了一次壓測,測試的頁面是真實的播放頁。發現渲染的耗時是隨著併發數量的增長,而增加。如果想使渲染時間保持在 1秒以內,那麼併發數的極限是 40個,難道只能無限堆機器?這肯定不是個辦法。
40個開什麼玩笑,要知道播放頁每天的訪問請求是億級的,有時候來個搜尋爬蟲,那還不直接癱瘓。(後面當然解決了,在這個標題下就不詳細說了。)
二、用了vue那肯定是按照產品模組一個個的開發元件,那播放器那塊肯定是一個單獨的如VideoPlayer.vue,頁面接入播放器是以SDK的形式接入 程式碼應該如下:
<template lang="html"> <div id="bofqi" ref="bofqi"></div> </template> <script> export default { props: { aid: { type: Number, default: null }, cid: { type: Number, default: null } }, mounted(){ // 初始化播放器 new EmbedPlayer("player", `cid=${this.cid}&aid=${this.aid}&autoplay=true`) } } </script>
上面是一段虛擬碼 初始化播放器需要傳入aid和cid 並且頁面上必須要有一個id為 #bofqi
的容器 這樣播放器就會正確的被例項化
emmm 看著很正常沒毛病的一段, but!初始化播放器是在vue的mounted生命鉤子裡面(也只有這裡才能拿到 #bofqi的容器
) 那意味著要等元件完全渲染完成後才能初始化播放器。可怕~ 看圖:
這樣的話,播放器要在很後面才能去初始化,後面還有請求介面、拉視訊流等畫面出來那就很後面了,花都謝了~

image.png
所以這裡需要改進優化下,最直觀就是儘快初始化播放器最好是和模板頁面同步執行。這裡我們嘗試了兩種優化方式:
一、把#bofqi
容器提出來放到html模板裡,然後在容器的
dom
注入一段初始化播放器的程式碼,事實上我們有幾個版本上的優化就是這麼做的。如下(虛擬碼):
<body> ... <div id="app"></div> <script src="//s1.hdslb.com/bfs/static/player/main/video.js"></script> <div id="bofqi"></div> <script>new EmbedPlayer("player", `cid=${cid}&aid=${aid}&autoplay=true`)</script> ... </body>
這裡有個小問題,就是播放器這個容器的定位,因為脫離了了文件流,是懸浮在整個頁面之上的,在前面的幾個版本當中播放頁只有寬窄兩個螢幕的定位 所以相容上還好,固定寫死兩個尺寸下的CSS就好了。大大提前了初始化的時機,上線後效果很好,基本上SDK下載完成就開始初始化了,不用等頁面渲染。

image.png
#bofqi
容器再放回vue元件裡,這個問題當時確實糾結過一陣,不過最後還是和架構的同學一起討論想出了個黑科技,用過vue的同學都知道在template裡面是無法插入script標籤的。但是為了滿足自適應只能把容器放回文件流中,同時還需要插入一段初始化播放器的js程式碼。解決方法如下:
App.vue
<template> ... <div class="player-wrap"> <!-- bofqi容器放回到元件中 --> <div id="bofqi"></div> <div v-html="innerScript"></div> </div> ... </template> <script> export default { data() { return { innerScript: '' } }, created() { // created 鉤子會在服務端執行 所以這需要判斷 if(typeof window === 'undefined') { this.innerScript = `<script type="text/javascript"> new EmbedPlayer("player", `cid=${cid}&aid=${aid}&autoplay=true`) <\/script>` } } } </script>
利用created鉤子和v-html的特性在服務端給template注入一段指令碼。目的達到,而且在客戶端vue對比的時候也會通過驗證。 完美解決~
總結:框架是個好東西,可以解決我們在開發中面臨的大部分問題,但不是所有問題。
注意:注入程式碼最後script的格式<\/script>
載入和執行避讓 控制每一個請求
任何一個頁面都是由很多個模組組合而成的,這裡面可以把所有模組做個分類,挑出核心的、重要的、一般的,然後排個優先順序,這個標準的衡量可以站在使用者的角度出發,視訊播放頁的核心當然是播放器模組,除此之外其他的都可以算是修飾的,視訊資訊、up資訊、評論、tag、推薦列表等之類的都可以排在後面一個級別。
我們都知道JS是單執行緒的,是一件一件的來處理事情,放在最前面的優先處理。所以在店裡只有一個服務員的時候,排隊往往是最高效的,那麼同理在頁面載入資源和JS執行的時候也可以這樣去給它們排個隊。
資源載入上雖然瀏覽器裡面可以同時發出多個請求 同步操作,但是要考慮到80分位以上偏後的使用者可能他們的電腦效能和頻寬的情況下,請求的併發可能會帶來更糟糕的體驗,同時處理多個任務,電腦會更加卡頓,在這裡同步並沒有優勢,所以讓資源的載入也來排個隊是很有必要的。另外如果JS模組沒有特意去設計的時候,大多數js檔案在下載下來的瞬間其實就自執行了比如manifest/vendor等,在不影響開發的體驗上很難改變js的執行時機, 基於以上我們想到的是用鉤子函式去回撥,然後把要控制的部分做成序列執行,這樣就能控制這個時間段的所有資源的下載和執行了 。流程圖如下:

image.png
上圖中可以看到,頁面的第一個請求HTML模板下載 在解析過程中遇到播放器的SDK js資源然後同步下載 下載完成後 立即初始化播放器 播放器內開始傳送請求視訊流,第一幀回來後觸發鉤子(PlayerMediaLoaded頁面和播放器約定好的) 下載頁面其他資源,因為是服務端渲染所以這裡不用擔心頁面上什麼都沒有,骨架和一些必要的資訊都已經出來了。為了確保頁面的完整性,這裡做了個兜底,及時鉤子沒有生效 4秒後也會觸發下一步操作。 這樣一來就把所有不需要第一時間的請求和執行延後處理了,優先保證了播放器視訊首幀先出來。
細節處理,為了讓播放器相關的資源優先和真正控制好這個時間段內的每個請求,應儘量避免模板裡面帶出來資源載入比如 圖片 小圖示之類,除非這些是SEO必需的或者是首屏關鍵用到的CSS背景圖之類,能沒有就沒有,否則就不好真正控制到每一個請求的發出。
資源指令碼延後載入執行
這個優化其實也是控制好每個資源載入和執行的時機,單獨拎出來說是想舉兩個例子。
第一個,在做優化的過程中,也有其他的任務混進來,由於我司的PV上報收集是基於前端的JS指令碼來做的上報,有一段時間PV的資料波動較大,懷疑是上報指令碼出了什麼問題,所以就在線上埋了個204請求來校驗和上報指令碼PV的差值來對比,當時想一個204請求是很快的而且不需要返回,結果上了第二天看資料,整個80分位播放頁首屏資料增加了近100ms,但50分位基本上沒什麼變化,204下了資料就回復了。 這個結果告訴我們在80分位+的段位上,任何風吹草動對首幀資料的影響來說都有可能被放大
另外一個,由於播放頁裡有些其他模組(廣告、評論等)需要用到jQuery,所以一直沒有去掉,預設情況下也是放在head標籤裡面比較靠前的位子,感覺這個可以把它後置到首屏完成以後再載入,所以我們花了一些時間改造,關鍵模組不在依賴JQ,做了後置處理,第二天的資料大概下降了70ms左右。
檔案體積優化
減少檔案體積是一個通用的優化,體積的大小直接影響到網路下載的時長,減少控制檔案大小的方式也有很多,比如服務端常用的檔案壓縮(gzip、br...),前端去不必要的依賴、框架等,很多JS框架也是儘量精簡來獲得市場優勢,檔案體積的影響對於PC上的影響還算可以,如果是H5那是必須嚴格控制的,網路環境的差異也直接反應到頁面載入速度的體驗,同樣在極限優化的情況下,不管是H5還是PC那肯定是儘量精簡。
最初的播放器js的sdk核心庫大概在1M左右,裡面不僅是視訊初始化還有彈幕、高階彈幕、解碼、各種設定等等,其實完全沒必要,後來播放器組的同學也給播放器做優先順序劃分,把功能拆開,做了很多工作大幅度精簡核心庫最後優化至200K左右,體積小了好多倍,80分位速度提升明顯。
還有HTML模板,這是第一個請求,後面發生所有的資源請求都基於它,所以它的體積也尤為重要,所以在這個裡面我們除了該有的骨架、資料和SEO需要的部分儘量精簡,包括把頂導做成後置SDK引入的方式,也是為了減少體積,SSR的時候頁面上會輸出大量的資料( INITIAL_STATE ),其實有些欄位用不上,所以在服務端渲染的時候要清洗一下,能刪的地方都刪了,最終從幾十K 優化到 十幾K。
前置playurl(Node)
正如前面所說,播放頁是基於node服務做的ssr,node這一層也是我們前端負責的,所以可以很好的利用這個點去做一些事情。先看下播放器從初始化到出現首幀有哪些關鍵步驟。如下圖:

image.png
從圖上可以看到,把一個前端請求的介面後置到後端去處理,這樣播放器去請求視訊流的時候直接從模板裡拿到了地址,不用再去發非同步。一個前端非同步請求的介面一般情況在幾十到上百毫秒,放到後端去處理走的還是內網介面(後端對內網介面返回時長控制在20ms以內) 又更快了。
這個優化有個注意的點,因為播放頁是ssr同構的方案,在頁面降級的情況下服務端是不會輸出資料的,頁面會走客戶端渲染,這時候playurl就拿不到了,所以還是會和播放器本身配合的去做,播放器之前的邏輯還是不能去掉,需要做個判斷,如果頁面上能獲取到視訊流的地址就直接用,否則自己發個請求去獲取。
推薦列表預取playurl
當你能提前知道使用者在頁面上的瀏覽行為的時候,其實你也可也以做點事情,比如播放頁的右側視訊推薦列表就是個很好的場景。通過資料發現播放頁內部跳轉的點選流量很大,大部分來自於推薦列表位 如圖:

image.png
上圖為播放頁推薦列表的點選熱力圖,從圖上可以清晰的看到靠在前面的視訊卡片點選量很大(和它在第一屏有直接關係) 。利用這個點 我們可以給這些點選量大的卡片去預取視訊地址,這裡我們是取了前4位的地址。
細節點:在預期地址發請求的時候需要判斷下當前視訊和頁面的狀態,最佳時間是頁面空閒狀態下,避免和其他任務並行,影響其他正常模組的效能。SPA過載優化
與之前的版本(17年之前)相比,新版播放頁已經是一個單頁應用了,點選視訊推薦列表,所有模組區域性更新,不用跳新視窗再走一套了,最重要的是播放器不用重新初始化和資源不用重新載入了。這樣一來整個頁面就能省下很多前置的耗時。
雖然說頁面已經SPA了,但是在不注意的情況下也會有一些效能上的問題,如圖:

image.png
上圖分為兩個部分,優化前和優化後,在優化前 每次使用者點選推薦列表後路由先切換,因為頁面上的模組的更新基本上都是基於watch路由上aid來做改變的,再由aid去請求播放頁資訊介面,獲取到對應的cid 然後傳給播放器過載視訊。這裡會有個問題每次都會消耗一個介面請求的時間來獲取播放器過載必要的aid和cid。其實我們在推薦列表裡面是能直接獲取到視訊的aid和cid的。
我們故技重施優化了一波。直接把aid、cid傳到播放器 先實現過載,在首幀回來之前我們頁面上所有需要更新模組不觸發更新,等待播放器鉤子通知再去做各自元件的更新。所以元件裡之前的watch aid的方法都變成了事件的方式來通知。
這裡主要還是用到了上面說的資源載入和執行的避讓以及一些邏輯處理上的優化,最終給過載事件帶來了100多毫秒的收益。
靜態資源Prefetch
預載入靜態資源已經是優化中常見的一種優化手段了,通過流量較大的入口頁面給需要優化的頁面帶資源快取,這樣在使用者訪問播放頁的時候就不需要重新請求資源而是從本地獲取(from disk cache),由於prefetch的優先順序比較低,( network -> priority: Lowest
),所以不用太擔心當前頁面的載入帶來的效能問題。也可以通過多個頁面一起來做,首頁、搜尋、空間這些頁面都是PC上的大流量頁面而且有大量的視訊開片往播放頁跳轉,所以非常適合。程式碼:
<link rel="prefetch" as="script" href="//s1.hdslb.com/bfs/static/player/main/video.js">
之前查過一段播放頁的資料,結果顯示帶有這些頁面reffer的播放頁要比其他的請求快200ms左右。
階段總結
在技術沒有本質變化的情況下,優化並不是什麼特別高深的技術,大多數情況都是你願不願意去想且去做的事情,有很多非常細節的地方可以去做優化,有時候我們猶豫不決,經常會感覺收益不確定、或者是這個優化可能會對現有程式碼的整潔性造成很大破壞,後面不好維護等等,不太想去做。其實這個時候可以通過實驗的方式來驗證,最終你會得到一個收益值和付出成本衡量來決定是否需要採用這個方案。 從本質上講作為一個前端開發 凡是對頁面效能有提升、對使用者體驗優化的事,我們應該盡全力去做,特別是這種流量巨大的頁面,可能我們優化了一點點,但收益是千千萬。在一些重要的頁面上,程式碼的維護性、擴充套件性都不是我們首要考慮的,我們在乎的重中之重應該是頁面的效能和使用者的體驗。
先分享這麼多 完結撒花~