“秒开”浏览器实现起来有多难?
作者:billpchen,騰訊看點前端開發工程師
近兩年,信息流行業處于一個增長緩慢甚至停滯的狀態,騰訊看點一直在尋求自己的破局之路。研究發現,近兩年實現爆發增長的業務都具有用戶覆蓋面大、差異化小的普適特點。
什么內容具備普適特點呢?有兩類,一類是打發時間、放松解壓的搞笑內容,一類是明星八卦、話題談資的熱點內容,這兩類內容具有低門檻、 快消費、易傳播的特點。為了進一步降低內容消費的門檻,我們把消費場景放在了信息流中,用戶無需進入詳情頁就可以直接消費完文字、圖片、動圖、視頻等內容,這種新的內容形態被稱為“短內容”,由短內容構成的信息流被稱為“短內容頁面”。
QQ 瀏覽器中短內容頁面的入口是在推薦流中的短內容卡片,一般帶有分享、評論、點贊互動欄的就是短內容卡片,點擊短內容即可以進入短內容頁面。
對于 C 端頁面,用戶體驗尤為重要,尤其是首屏體驗,更是奠定了用戶的第一印象,所以“性能優化/首屏優化”常作為前端人的重要研究課題。那短內容頁面的首屏體驗是怎么樣的呢?
看完上面在手機性能相對較好的 iPhone X 上的演示動圖,你一定會感覺到這真是個糟糕的體驗,那到底糟糕在哪里呢?具體有三點:白屏時間長、圖片加載慢、頁面過渡僵硬。本地的首屏優化方案就集中在這三方面,本文也圍繞這三點詳細闡述。
1. 白屏時間長
所謂白屏,指打開新頁面時屏幕中沒有任何有意義的內容,只有無休無止令人窒息的空白。造成頁面白屏的原因有不少,比如頁面崩潰、網絡資源加載較慢、頁面啟動卡頓等等,這里不討論頁面崩潰等程序出現 bug 的場景,有 bug 就去解決嘛。
我們都討厭白屏,有時白屏時間比較短,在我們的容忍范圍內,但有時白屏時間很長,那就令人煩躁了。據統計,大多數用戶可以忍受 1000ms 以內的白屏時間,超過 1000ms 時隨時間的變長越來越無法忍受。所以我們的首屏優化目標是 1000ms 以內,即“秒開”。
定好了目標,下面貌似就應該行動起來尋找解決方案了,但等等,我們一直在說“首屏”,那什么是首屏,我們得先嘮嘮。首屏的英文名是 “Above the Fold”,Fold?跟折疊有什么關系?“Above the Fold” 這個概念最早用于出版行業,買過報紙的同學都知道,因為方便搬運,報紙的一般都是折疊起來的,即使是“頭版”,也分朝上和朝下的一面,如圖所示,朝上的那一半被稱為 “Above the Fold”,也是被報社認為最重要的位置。延伸到互聯網產品,“Above the Fold” 用來指代頁面不用操作(比如點擊、滾動)就能看到的信息。
明白了“首屏”,那什么是“首屏時長”?雖然“首屏”這個概念是從報紙那里借鑒過來的,但我們看到報紙的頭版也就看到了,不存在先看到一部分再看到完整的,所以“首屏時長”是屬于互聯網產品特有的概念。為了衡量看到“首屏”內容的效率,人們定義了很多標準,比如 Google 就定了 FP、FCP、FMP 等很多指標來衡量首屏的性能,久而久之,這些標準成了大家公認的標準。短內容頁面基于 Hippy,一種動態化框架,本身沒有什么衡量標準,所以我們就仿照 Chrome,定義了動態化頁面的首屏打開性能指標:FCP 和 FMP。
在我們的定義中,短內容頁面的 FCP 指的是從外層入口點擊的時間點到短內容頁面根元素 didMount 的時間點的差值,FMP 指的是從外層入口點擊的時間點到短內容第一條卡片根元素 didMount 的時間點的差值。這里的 FCP 和 FMP 可能跟 Chrome 中的定義有所偏差,不過無傷大雅,我們需要的只是有那么幾個指標來衡量優化前后的效果。
明確了首屏時長的衡量標準后,那下面我們就可以開始正式的優化環節了。從哪里下手呢?既然要縮減白屏時長,那就要了解首屏加載有哪些環節,辨別出關鍵的耗時環節才能有的放矢。
如圖所示,當用戶點擊了短內容頁面的入口時,客戶端開始創建 Activity,然后開始創建 Hippy 引擎,引擎創建完成后加載 Bundle,而后向前端發送 loadInstance 事件開始啟動業務,接著便開始渲染 HippyRootView,下面的事前端就比較熟悉了,拉取數據,渲染內容。
整個過程可以分為兩個部分:頁面啟動和數據加載,我們分別從這兩方面進行優化。
1.1 頁面啟動
頁面啟動階段主要有 initEngine、onInitialized、loadModule 和 loadInstance 4 個階段,逐個分析發現,在“加載 bundle” 階段我們可以有所作為。加載 bundle 的時間跟 bundle 體積成正相關,如果我們把 bundle 的體積減小,那么 bundle 自然加載地更快。如何減包呢?我們可以使用 Webpack Bundle Analyzer 對 bundle 進行分析。
從分析結果中,我們可以看到有近 10 個文件的體積超過了 100 KiB,還有很多的文件達到數十 KiB,那開始挨個分析每一個文件。雖然 CircleCommJce.js 體積最大,但項目處于新舊交替過程中,項目原先是通過 WUP 協議拉取數據,而現在隨著騰訊看點三端(QQ 看點、QQ 瀏覽器、看點快報)的統一,CGI 服務也統一成 HTTP,所以這里逐漸會將 WUP 協議換成 HTTP 協議,等項目協議切換完成,這些與 WUP 相關的文件都可以刪掉了。與 CircleCommJce.js 類似的還有 MTT4PageInfoJce.js 等等,這些文件我們暫時不作改動。
接著,lodash.js 這個文件引起了我的注意。Lodash 是一個便捷的 JS 工具庫,但便捷的代價就是項目體積的增加,這不,lodash.js 就有 466 KiB。那如何減少這個文件的體積呢?很簡單,徹底不用。在項目中檢索 “lodash”,發現被引用了 19 次,共 45 次調用。乍一看還挺多,仔細梳理,發現一共就用到了 Lodash 的 5 個方法:_.get、_.chunk、_.pick、_.pickBy 和 _.mapValues。將這些方法用自己寫的 JS 函數替代,比如下面就是 _.get 的簡單實現。
將 lodash.js 刪掉后,打包后的 bundle 體積減少了 2.4%,安卓的啟動時長減少了 5.2%,iOS 的啟動時長減少了 3.9%。
1.2 數據加載
數據加載階段主要包含 fetch 和 setContentView 兩個階段,逐個分析發現,主要的耗時環節是獲取數據環節,那如何優化這部分的耗時呢?我們再次細化獲取數據環節,大概分為 DNS - 建立連接 - 后臺處理 - 個性化推薦 - 數據返回這幾個階段。網絡傳輸環節與運營商相關,個性化推薦與算法、機器數量和性能相關,即便優化,耗時也很難有實質性的縮減。總而言之,從減少獲取數據耗時這一環節本身出發,我們做不了什么。那是否意味著這個環節我們不能優化了呢?方法還是有的,我們可以提前獲取數據,然后緩存到本地,等用戶打開頁面的時候直接從緩存獲取第一刷的數據。從緩存讀取數據會比從網絡讀取數據減少不少的耗時,那具體怎么做呢?
短內容頁面的入口是推薦流中的短內容卡片,當推薦流中有短內容卡片曝光時,QQ 瀏覽器將會去網絡拉取一刷數據緩存到本地。如果用戶真的點擊了短內容卡片進入了短內容頁面,那么會直接從緩存中獲取數據。這個方案被稱為“數據預加載”。
道理我都懂,但感覺太簡單,里面會不會有坑呢?有的,數據預加載可能存在四個方面的問題。
流量浪費
如果用戶沒有進入短內容浮層,那這部分流量不是浪費了嗎?這里要分兩方面看。一方面是公司的流量浪費,公司的流量費用是與公司峰值帶寬相關的,數據預加載對公司的峰值帶寬影響不大,所以不會導致公司方面的流量浪費;另一方面是用戶的流量浪費,據統計有 44% 的短內容用戶用流量瀏覽,對于這部分用戶來說,預加載方案是一種以空間換時間的方式,這部分的流量浪費不可避免,并且為了減少流量浪費,我們選擇了預加載 3 條而不是更多,因為一般情況下 3 條恰好可以覆蓋一屏,包含 1 條主 TL 的帖子和 2 條推薦帖子。
推薦浪費
用戶看到的帖子都是由推薦系統推薦的,已經看過的帖子推薦系統就不會再給我們推送了,這叫“曝光去重”。在數據預加載場景下,很可能會出現推薦系統推薦了帖子,但用戶實際上沒有進入短內容頁面也就沒有消費這些帖子的情況,這叫做“推薦物料浪費”。針對這種情況,我們與推薦后臺約定,預加載出的推薦數據在下一次推薦時不會被曝光去重,只有用戶真正消費的時候,前端回寫曝光數據,告訴推薦后臺哪些帖子被真正消費了,那么這些帖子才會被曝光去重,那么這樣也就避免了推薦物料被浪費的情況。
緩存關閉
預加載的數據會被瀏覽器緩存在內存中,當瀏覽器運行在前臺時,手機分配的內存空間足夠;而當瀏覽器切到后臺時,手機分配的內存空間減少,會導致預加載數據的緩存空間被清除,這樣不僅之前緩存的預加載數據都被清除,下一次寫緩存數據時也會失敗。為了解決這個問題,只要每次寫緩存時檢查一下緩存空間還在不在,在的話就就直接寫,不在的話就得重新創建緩存空間。
二次打開
當用戶退出短內容頁面時,大約會有 3% 的用戶會重新進入。第一次打開短內容頁面的時候,推薦的 2 條數據會被清除,緩存中只會留下第一條數據。用戶第二次進入的時候,只會讀取那一條緩存的內容,其余的需要從網絡拉取。
1.3 Bundle 預加載
分析了首屏渲染的各個關鍵耗時環節,我們縮減了 bundle 的體積,提升了頁面啟動的速度;同時對數據進行了預加載,第一刷的數據直接從緩存中讀取,提升了數據拉取速度。除此以外,還有其他優化方法嗎?減包加速頁面啟動終究還是有瓶頸,何不一步到位直接讓頁面運行在后臺,等到用戶真正點擊的時候再把頁面提升到前臺呢?
這個方案的原理跟手機中的 APP 很相似,如果一個 APP 運行在后臺,那被切到前臺時將會很快,但如果被第一次打開,耗時將會較長。所謂的 bundle 預加載,就是將短內容頁面預先加載在瀏覽器的后臺,等到用戶點擊打開頁面時再顯示出來。這一過程的演示:
視頻中透明的浮層代表沒有任何內容的短內容頁面,當瀏覽器啟動時,會在背后悄悄啟動一個空白的短內容頁面,如果用戶點擊了入口短內容卡片,那么這個空白的短內容頁面將會被提升到瀏覽器的最頂層,并且被渲染。這樣,bundle 加載的耗時將會被大大縮減。除此以外,瀏覽器還會在后臺又啟動一個空白的短內容頁面,以備下一次用戶打開短內容頁面使用。
1.4 小結
為了解決白屏時間長的問題,我們仔細剖析了頁面加載中的每一個環節,其中針對“加載 bundle”和“獲取數據”兩個關鍵耗時環節采取了“減包”和“數據預加載”措施,同時也認識到“減包”所帶來的收益是遞減并且有瓶頸的,所以直接采用“bundle 預加載”的方式,在瀏覽器啟動時準備一個運行在后臺的空白短內容頁面,用戶打開時直接使用該空白頁面,大大縮減了頁面啟動了時間。
2. 圖片加載慢
從網絡加載圖片資源需要一定的耗時,所以時常會出現文字已經展示但圖片還是一片灰色的情形。那如何縮減圖片的加載時長呢?我們探索出了 4 個方案:圖片壓縮、圖片裁剪、SharpP 和圖片預加載。
2.1 圖片壓縮
縮減網絡傳輸時長,最簡單有效的方式便是減小網絡包的大小,圖片也不例外,所以我們很自然地想到了壓縮圖片。那如何壓縮圖片呢?得益于騰訊強大的技術基礎建設,我們有比較完善的圖片服務。
這是一條常見的圖片 CDN 地址,主要包含 CDN 域名、圖片平臺業務ID、File ID 和壓縮寬度幾個部分,我們關心的圖片壓縮就跟這最后一個部分相關。
在上面的鏈接例子中,/0 指的是原圖,除了 /0 外,還有 /900、/600、/320、/200 和 /180 共計 6 種配置,/900 的意思是圖片最大的寬度是 900 像素,如果原圖寬度大于 900 像素就會被等比縮放到 900 像素,如果小于 900 像素則保持不變。
那問題來了,該選哪一種壓縮比例呢?
以 iPhone 11 為例,橫向上像素值為 828。如果圖片寬度大于 828,那么在 iPhone 11 上展示時就很清晰;如果圖片寬度小于 828,那么在 iPhone 11 上展示就會被拉伸,原圖寬度越小模糊感就越強烈,所以要想圖片在 iPhone 11 上展示清晰,需要寬度大于 828 像素,所以只有壓縮比例為 /0 或 /900 的圖片符合要求,挑選最小的尺寸即可,即 /900。同樣的,如果手機屏幕寬度只有 400 像素,那么圖片的寬度大于 400 像素即可,/0、/900 和 /600 都符合要求,我們選擇尺寸最小的 /600。
明白了這個道理,我們看實際應用中的情況。上面的例子中是雙圖的場景,即兩張圖片并列排版,這樣每張圖片的展示區域最大是 828 / 2 = 414,也就是圖片的寬度大于 414 像素就足夠清晰了,滿足條件的有 /0、/900 和 /600 三種壓縮尺寸,我們選擇最小的 /600。
再看三圖的場景,每張圖片的展示區域最大只有 828 / 3 = 276 像素,所以 /320 已經足夠滿足需要了。
綜上所述,圖片的壓縮尺寸由圖片的展示區域決定,而展示區域由手機分辨率和圖片的排版決定。
2.2 圖片裁剪
那有沒有辦法再減小圖片的大小呢?有的,圖片裁剪。
短內容頁面中多圖都是以 1 : 1 展示的,也就意味著超出 1 : 1 的部分不僅不會展示,還會增加圖片的大小導致圖片加載耗時變長。這樣的話,我們只需要加載 1 : 1 的部分就可以了。理想很豐滿,現實也很性感,強大的圖片服務也提供了圖片裁剪功能!
不過不是所有的圖片都支持裁剪,只有滿足業務 ID 為 “qq_public_cover” 并且 File ID 以 “_open” 結尾的圖片才能夠裁剪。一共支持 16 種裁剪尺寸,239 ?95、358 ?143、564 * 280……其中數字僅表示裁剪比例,不表示圖片寬度。
上圖顯示了按一定比例裁剪后的圖片的 3 種樣式。
因為多圖場景下圖片都是 1 : 1 展示的,所以我們只需要拉取對應尺寸的圖片,即在 File ID 末尾加上 “_280_280”。上面的例子中,原圖 26KB,裁剪后的圖片只有 18KB,圖片體積縮減了 30%。
這里需要提一下的是,圖片裁剪服務原本的使用場景并不是為了縮減圖片體積,而是為了突出圖片主體。我們是不是經常遇到因為圖片中人物頭像在頂部,在信息流中展示時頭部被裁切掉,然后只能看到半截身子的情況?裁圖服務可以將原圖裁剪成不同比例,并且保證每種比例都會突出圖片主體,比如人物、動物、物體等等,信息流業務在使用圖片時選取一種與展示比例相近的裁剪尺寸,這樣展示的圖片可以較好地突出主體。下面的例子可以很形象的展示是否使用裁剪過的圖片的不同效果,前者使用原尺寸,主體頭部被裁切,后者使用 280 * 280 比例的裁剪,主體依舊完整。
2.3 SharpP
前面兩個都是縮減靜圖加載時長的方法,那動圖呢?
動圖我們采用了 SharpP 格式,這是騰訊自研的圖片壓縮技術,對標業界的 WebP,不過卻比 WebP 有更好的壓縮效果。
2.4 圖片預加載
無論是減包還是 SharpP,總會有一定的網絡耗時,如果還想減少圖片加載耗時應該怎么辦呢?沒錯,預加載圖片~
當推薦流中短內容卡片曝光后,瀏覽器會去請求第一條短內容的圖片并緩存起來。短內容頁面打開時,直接使用緩存的圖片,這樣就可以避免長時間只能看到圖片灰底的情況。不過為了節約流量,目前只會預加載第一條短內容的圖片。下面的例子中,正文圖片很快加載完成,與此形成鮮明對比的是尺寸很小的頭像,在正文圖片加載完成后仍然還是灰底。
2.5 小結
為了解決圖片加載慢的問題,我們采取了壓縮圖片和裁剪圖片這兩個方法縮減靜圖的體積,而對于動圖采用了 SharpP 格式。為了進一步提升首屏體驗,我們對第一條短內容的圖片進行了預加載。
3. 頁面過渡僵硬
從推薦流進入短內容頁面過程很僵硬,可以感覺到就是頁面間的跳變,用戶體驗不佳。那該如何優化呢?在調研了瀏覽器現有的幾種頁面打開方式后,我們決定采用“側滑”的過渡動畫,即短內容頁面從右往左切到瀏覽器的前臺,如圖演示:
4. 小結
站在用戶角度,以用戶的眼光審視短內容頁面的首屏體驗,我們發現了三大問題:白屏時間長、圖片加載慢和頁面過渡僵硬。
針對白屏時間長的問題,我們梳理了首屏的關鍵耗時環節,再分析縮減每個環節耗時的可能性,其中在 loadModule 環節我們采取了減包的方法,在 fetch 環節我們采取了數據預加載的方法。更進一步的,我們通過 bundle 預加載的方法,瀏覽器在啟動時會在后臺加載一個空白的短內容頁面,用戶點擊短內容卡片時,再將空白的短內容頁面提升到前臺并且渲染數據。
針對圖片加載慢的問題,我們采用圖片壓縮和圖片裁剪的方式縮減信息流中的圖片體積,同時使用 SharpP 格式替代傳統的 Gif 來縮減動圖的體積。更進一步的,為了首屏的體驗,我們預加載了第一條短內容的圖片。
針對頁面過渡僵硬的問題,在調研了瀏覽器現有的幾種頁面打開方式后,我們采用了新頁面從右邊“側滑”的過渡動畫。
經過這一系列的優化,首屏性能指標和產品指標都有顯著的提升。
先看平均耗時。在優化前,首屏 FMP 平均為 2.9s,接入數據預加載后平均 FMP 為 0.75s,再接入 bundle 預加載后平均 FMP 為 0.6s。
再看耗時分布。在優化前,FMP 在 0.5s 內的訪問占 0%,1s 內占 2%,2s 內占 39%;接入數據預加載后,FMP 在 0.5s 內占 44%,1s 內占 82%,2s 內占 97%;再接入 bundle 預加載后,FMP 在 0.5s 內占 59%,1s 內占 87%,2s 內占 98%。
彩蛋~
想了解鵝廠程序員有多硬核?
有哪些歡樂沙雕日常?
快來視頻號找我們!
掃碼一鍵關注騰訊程序員
總結
以上是生活随笔為你收集整理的“秒开”浏览器实现起来有多难?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PB 级大规模 Elasticsearc
- 下一篇: Linux CPU 性能优化指南