vivo 悟空活動中臺 - H5 活動載入優化
本文首發於 vivo網際網路技術 微信公眾號
連結: https://mp.weixin.qq.com/s/6gtVR0nVNcZvREjwftZgzA
作者:悟空中臺研發團隊
【悟空活動中臺】系列往期精彩文章:
- 《揭祕 vivo 如何打造千萬級 DAU 活動中臺 - 啟航篇》 主要為大家講述 vivo 活動中臺的能力與創新。
- 《悟空活動中臺 - 微元件狀態管理(上)》介紹了活動頁內 RSC 元件之間的狀態管理和背後的設計思路。
- 《悟空活動中臺 - 微元件狀態管理(下)》探索平臺和跨沙箱環境下的微元件狀態管理。
- 《vivo 悟空活動中臺-基於行為預設的動態佈局方案》本文以“滿屏”場景下的頁面佈局思考為切入點,以微元件為元素單元,提供了一種新的佈局方案設計思路——基於行為預設的動態佈局方案,並詳細的分享了設計目的及具體實現方案。
- 《vivo悟空活動中臺 - 微元件多端探索》是基於自助多端擴充套件,也就意味著多端 微 元件選擇越豐富,內容越通用,玩法越多樣,產品價值也會越高。
一、背景
通過之前悟空活動中臺系列文章,大家對微元件、動態佈局等技術方案有了一定的瞭解。本篇我們帶大家瞭解下悟空H5專題效能優化之路。
在移動網際網路時代,H5頁面載入體驗至關重要。消費者行為和觀念也會受到頁面載入時間的產生顯著影響,最明顯的就是我們現在很難去等待一個頁面載入超過三秒的頁面,尤其是年輕人。專注效能測試的SOASTA公司曾發表過結論:移動端載入每耗時1秒, 影響轉化率最高可達 20%。
在營銷中臺業務快速發展過程中,悟空始終把網站響應速度和使用者體驗放在第一位,通過技術創新,不斷尋找最優載入方案,取得了很好的效果。下面我們就一起來探索下。
二、優化歷程
每談到效能優化,前端er就能聯想到一道經典面試題:從輸入URL到頁面載入,瀏覽器都執行了什麼?
體驗優化的歷程和這道題一樣,需要系統化梳理、體系化實踐。我們從網路、資源、渲染、執行層出發,不斷探索載入優化方案。
1、網路層優化
(1)DNS 處理:增加 dns-prefetch
瀏覽器對網站第一次的域名 DNS 解析查詢流程依次為:瀏覽器快取 >> 系統快取 >> 路由器快取 >> ISP DNS 快取 >> 遞迴搜尋。
移動端環境下,DNS 請求頻寬非常小,但延遲很高。針對該問題,我們採取預讀取DNS方案,該方案能顯著降低延遲,平均載入時長可減少1秒左右。
為幫助瀏覽器對某些域名進行預解析,我們對上線活動 html 文件中新增 dns-prefetch標籤。加入該標籤後,瀏覽器解析步驟如下:
第一步:用 meta 資訊來告知瀏覽器,當前頁面要做 DNS 預解析:
<meta http-equiv="x-dns-prefetch-control" content="on" />
第二步:在頁面 header 中使用 link 標籤來強制對 DNS 預解析:
<link rel="dns-prefetch" href="//topicstatic.vivo.com.cn" />
悟空在上線H5資源需要根據不同區域,生成不同的dns-prefetch地址,編譯活動腳手架link標籤新增邏輯如下:
<% if (國內活動) {%> <link rel="dns-prefetch" href="//topic.vivo.com.cn"> <link rel="dns-prefetch" href="//cmsapi.vivo.com.cn"> <link rel="dns-prefetch" href="//topicstatic.vivo.com.cn"> <% } else if(印度活動) {%> <link rel="dns-prefetch" href="//in-goku.vivoglobal.com"> <link rel="dns-prefetch" href="//topicstatic.vivo.com.cn"> <link rel="dns-prefetch" href="//in-gokustatic.vivoglobal.com"> <% } else { %> <link rel="dns-prefetch" href="//asia-goku.vivoglobal.com"> <link rel="dns-prefetch" href="//asia-gokustatic.vivoglobal.com"> <link rel="dns-prefetch" href="//asia-wukongapi.vivoglobal.com"> <% } %>
(2) CDN 分發優化
CDN 的全稱是 Content Delivery Network,即內容分發網路。CDN 是構建在現有網路基礎之上的智慧虛擬網路,依靠部署在各地的邊緣伺服器,通過中心平臺的負載均衡、內容分發、排程等功能模組,使使用者就近獲取所需內容,降低網路擁塞,提高使用者訪問響應速度和命中率。
下圖展示終端使用者訪問頁面時,CDN獲取過程:
快取對於CDN服務至關重要,合適的快取策略能夠降低源站的請求壓力,從而提升頁面載入速度,因此我們需要優化靜態資源儲存方式和快取策略。
CDN資源快取配置如下:
悟空將H5專題的靜態資源上傳至CDN,帶來如下提升:
- 通過 CDN 向用戶分發傳輸相關庫的靜態資原始檔,可以降低我們自身伺服器的請求壓力。
- 大多數 CDN 在全球都有伺服器,所以 CDN上的伺服器在地理位置上可能比你自己的伺服器更接近你的使用者。使用者直接訪問邊緣快取,極大地提升頁面資源的響應速度。
- 不快取HTML入口檔案,只快取js、css的策略,避免資源不更新的同時,加快了專題資源的獲取速度。
不快取HTML入口檔案的目的是防止客戶端快取策略,導致主入口資源不更新,導致線上升級失敗。
(3)HTTP/2
HTTP/2 的定義為:
(超文字傳輸協議第 2 版,最初命名為HTTP 2.0),簡稱為h2(基於 TLS/1.2 或以上版本的加密連線)或h2c(非加密連線)[1],是HTTP協議的的第二個主要版本,使用於全球資訊網。
將 HTTP 訊息分解為獨立的幀,交錯傳送,然後在另一端重新組裝是 HTTP 2 最重要的一項增強。事實上,這個機制會在整個網路技術棧中引發一系列連鎖反應,從而帶來巨大的效能提升:
_ |
1.0 |
1.1 |
2.0 |
長連線 |
需要使用keep-alive引數來告知服務端建立一個長連線 |
預設支援 |
預設支援 |
HOST 域 |
不支援 |
支援 |
支援 |
多路複用 |
不支援 |
- |
支援 |
資料壓縮 |
不支援 |
不支援 |
使用HAPCK演算法對 header 資料進行壓縮,使資料體積變小,傳輸更快 |
伺服器推送 |
不支援 |
不支援 |
支援 |
HTTP2.0開啟方式如下:
server { listen 443 **ssl** **http2**; server_name yourdomain; …… ssl on**; …… }
開啟 HTTP 2監聽:
listen 443 ssl http2;
多路複用代替原有的序列以及阻塞機制,使得多個資源可以在一個連線中並行下載,不受瀏覽器同一域名資源請求限制,提升整站的資源載入速度。
(4)動態字型壓縮
字型檔案大小普遍在2M左右,H5活動頁面字型量有限,但僅僅為少量特殊文字全量引入字型檔案,頁面效能損耗非常大。與此同時,由於營銷活動的複雜性與多樣性,單純的圖片字型很難滿足多變的運營需求。
尋找滿足字型多樣性的同時,保證字型大小,是平臺需攻克的技術難點,最終,我們探索出一套適用平臺的動態字型壓縮方案。
字型壓縮,也可以被稱為字型子集化,可以理解為通過特定方式將中英文字從大字型檔案中剝離,組合成小字型檔案供頁面使用。
概念看上去有點抽象,我們先直觀感受下壓縮前後效果:
接下來會重點講述悟空基於業務場景的字型壓縮方案,壓縮字型的核心訴求是:可壓縮字型檔案,可動態更換文字內容進行壓縮。
基於悟空微元件動態打包上線方式,我們選擇使用fontmin來完成動態壓縮字型。
動態壓縮字型分為以下幾個步驟:
第一步,讀取特定配置檔案中的 id,預先請求到對應頁面介面資料,進行資料歸集處理。部分程式碼示例:
const request = require('request') request(url, (error, response, data) => { if (error) { console.error(err); return } const res = JSON.parse(data) if (res.code === 0) { //獲取專題配置資料 const config = JSON.parse(URLDecode(res.data.config)) const pages = config.pages let str = '' const familyList = new Set() pages.forEach(page => { const items = page.items items.forEach(item => { //根據配置,拼接需載入字型的字串和字型型別 if (item.pluginInfo.enName === 'site-text') { str += item.pluginConfig.pureText familyList.add(item.pluginConfig.typeFace) } }) }); //處理字型 handleFont(str, familyList) } });
第二步,遍歷字型型別列表familyList,利用fontmin進行字型檔案壓縮。這一步要求我們預先將字型的本地檔案放入編譯腳手架中。在壓縮的同時,需要通過webpack外掛來生成對應的css檔案:
字型動態壓縮處理邏輯:
const compressFont = (fontText, fontName) => { const srcPath = `dist/${siteId}/font/${fontName}.ttf`; const destPath = `dist/${siteId}/compressFont`; const fontmin = new Fontmin() .src(srcPath) // 輸入配置 .use(Fontmin.glyph({ // 字形提取 text: fontText // 動態注入文字 })) .use(Fontmin.ttf2eot()) // eot轉換 .use(Fontmin.ttf2woff()) // woff轉換 .use(Fontmin.ttf2svg()) // svg轉換 .use(Fontmin.css({ fontPath: `/compressFont/`, fontFamily: fontName, })) .dest(destPath); // 輸出檔案 fontmin.run(function (err, files, stream) { if (err) { console.error(err); return } // 讀取生成後的對應的 css 檔案內容併合成 const fontCss = fs.readFileSync(path.join(__dirname, `../dist/${siteId}/compressFont/${fontName}.css`)).toString() fontStyleStr += fontCss loadHtml(fontStyleStr) }) } const handleFont = (fontText, familyList) => { familyList.forEach(name => { compressFont(fontText, name) }) }
2、資源優化
(1)圖片懶載入
圖片懶載入是一種很好的優化網頁或應用的方式,它能夠在使用者滾動頁面時自動獲取更多的資料,新獲取的圖片不會影響到頁面呈現,同時視口外的圖片有可能永遠不需要被載入,能夠極大的節約使用者流量以及伺服器資源。'
懶載入的一般形式表現為:
- 開啟首頁,滑動頁面
- 懶載入圖片展示預設圖
- 預設圖替換為真實圖片
根據悟空現有的技術棧,我們選擇vue-lazyload 去支撐位元件的圖片來載入:
- 對 vue 的原生支援,平臺擴充套件後所有元件都可使用
- 方便快捷的指令式開發,img 標籤的 src 改為 v-lazy 就可以實現圖片懶載入
- 功能符合預期,支援背景圖片懶載入,支援圖片 url 動態修改為 webp
悟空提供給元件開發者資源懶載入指令,使用者無需感知具體的載入邏輯,通過悟空的內建能力即可實現專題圖片懶加。具體用法如下:
<template> <div> <img v-lazy="imgUrl" /> <div v-lazy:background-image="imgUrl"></div> <!-- with customer error and loading --> <img v-lazy="imgObj" /> <div v-lazy:background-image="imgObj"></div> <!-- Customer scrollable element --> <img v-lazy.container="imgUrl" /> <div v-lazy:background-image.container="img"></div> <!-- srcset --> <img v-lazy="'img.400px.jpg'" data-srcset="img.400px.jpg 400w, img.800px.jpg 800w, img.1200px.jpg 1200w" /> <img v-lazy="imgUrl" :data-srcset="imgUrl' + '?size=400 400w, ' + imgUrl + ' ?size=800 800w, ' + imgUrl +'/1200.jpg 1200w'" /> </div> </template> <script> export default { data() { return { imgObj: { src: 'http://xx.com/logo.png', error: 'http://xx.com/error.png', loading: 'http://xx.com/loading-spin.svg', }, imgUrl: 'http://xx.com/logo.png', // String } }, } </script>
(2)圖片壓縮
在移動端環境下,圖片載入一直是需要重點優化的關鍵項,所以才延伸出懶載入這種互動方案來提高使用者體驗。
當該方案優化到了落地後,我們下一步考慮如何在保證圖片質量的前提下,儘量壓縮圖片體積,提升圖片載入效率。
WebP 是 Google 推出的一種同時提供了有失真壓縮與無失真壓縮(可逆壓縮)的圖片檔案格式。相比於其他相同大小不同格式的壓縮影象,WebP 格式的圖片擁有更小的體積以及更高的質量,所以它的優勢十分明顯。
WebP 是 Google 推出的一種同時提供了有失真壓縮與無失真壓縮(可逆壓縮)的圖片檔案格式。相比於其他相同大小不同格式的壓縮影象,WebP 格式的圖片擁有更小的體積以及更高的質量,所以它的優勢十分明顯。
在使用 WebP 進行有失真壓縮後,我們大概可以將原本的圖片大小壓縮至原來的十分之一左右,而圖片質量卻沒有大的損失。這確實是一個驚人的效率。
我們可以看下一組資料來看下 webp 有失真壓縮效果:
Webp 有失真壓縮(75%質量比)
await execFileSync(cwebp, ['-q', '75', filePath, '-o', webpPath]);
原大小 |
壓縮時間(ms) |
壓縮後大小 |
999kb |
237 |
38kb |
999kb |
221 |
38kb |
999kb |
228 |
38kb |
999kb |
228 |
38kb |
999kb |
261 |
38kb |
在轉換結束後,悟空會將原圖片和轉換後的webp圖片都上傳到cdn上,做一個備份的能力,實際業務場景可以根據需求去選擇是否使用 Webp 圖片。
下圖展示 Webp 壓縮前後效果,右側展示壓縮後圖片,圖片大小從215k減小至17k。
悟空在使用 Webp 壓縮時,也遇到種種問題,如下:
- 為什麼悟空選擇75%的壓縮質量?
- 什麼特徵的圖片不適合Webp壓縮?
- 部分圖片壓縮後資源變大
後續文章《悟空活動中臺 - 基於Webp的圖片高效載入方案》會詳細敘述悟空如何從平臺角度提供 Webp壓縮方案。
(3)跨域避免 option 請求
悟空H5專題採用的是前後端分離方案,伺服器域名和專題域名不一致,會受到瀏覽器同源策略影響。
我們發現數據主介面會發起兩次,其中第一個請求為預檢請求。
一般來說使用 application/json 的 post 請求是必然會帶入 OPTION 請求,何為 OPTION 預檢:
用於獲取目的資源所支援的通訊選項。客戶端可以對特定的 URL 使用 OPTIONS 方法,也可以對整站(通過將 URL 設定為“*”)使用該方法。
在CORS中,可以使用 OPTIONS 方法發起一個預檢請求,以檢測實際請求是否可以被伺服器所接受。預檢請求報文中的Access-Control-Request-Method首部欄位告知伺服器實際請求所使用的 HTTP 方法;Access-Control-Request-Headers首部欄位告知伺服器實際請求所攜帶的自定義首部欄位。伺服器基於從預檢請求獲得的資訊來判斷,是否接受接下來的實際請求。
有趣的是專題詳情為 GET 介面,為何 GET 請求也會發起 option 預檢?
這個原因得從簡單請求和複雜請求說起,跨域請求分為簡單和複雜兩種:
簡單請求:
請求方式為如下之一:
HEAD
GET
POST
HTTP 請求頭只能包含如下資訊:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type,但僅能是下列之一
application/x-www-form-urlencoded
multipart/form-data
text/plain
任何一個不滿足上述要求的請求,即被認為是複雜請求。一個複雜請求不僅有包含通訊內容的請求,同時也包含預檢資訊。
專題配置介面請求頭中帶有自定義 header,瀏覽器會認定為非簡單請求,需要向伺服器發出檢查,判斷該域名是否允許跨域。
經過分析發現,自定義 header 其實在此業務場景中非必傳自帶,發出預檢請求至少會有 100ms 的耗時,無形中延長頁面繪製時間。
最終解決方案:去除自定義header,修改為簡單請求,避免該請求發出預檢。
3、渲染執行優化
在網路層以及資源壓縮優化落地後,接下來探索瀏覽器渲染執行優化點,涉及到瀏覽器,一定會聯想到網頁解析過程,下圖清晰的展示靜態資源如何通過瀏覽器最終顯示:
當dom元素變化會導致瀏覽器重新執行渲染樹生成、繪製,我們稱之為重排重繪。
什麼是重排?當 render tree 中的一部分(或全部)因為元素的規模尺寸,佈局,隱藏等改變而需要重新構建。這就稱為重排(迴流)。每個頁面至少需要一次迴流,就是在頁面第一次載入的時候。
(1)避免重排
瀏覽器結構示意圖:
可以看到瀏覽器有負責解析、渲染請求內容的渲染引擎,哪些動作會導致瀏覽器重排:
(1)增加或刪除 DOM 節點;
(2)display:none(重排並重繪);visibility:hidden(重繪);
(3)移動頁面中的元素;
(4)改變元素尺寸(寬、高、內外邊距、邊框等);
(5)使用者改變視窗大小,滾動頁面等;
(6)頁面初始渲染;
(7)改變元素內容(文字或圖片等)。
offsetTop, offsetLeft,... scrollTop, scrollLeft, ... clientTop, clientLeft, ... getComputedStyle() (currentStyle in IE)
這些屬性都需要實時回饋給使用者的幾何屬性或者是佈局屬性,瀏覽器不得不立即執行渲染佇列中的“待處理變化”,並隨之觸發重排返回正確的值。
document.body.style.minWidth = '12OOpx' document.body.style.overflow = 'hidden' //獲取某div的偏移量 document.querySelector('xxx').offsetTop
我們優化活動程式碼執行邏輯,將上述直接操作 dom 的操作修改為 class 樣式操作,減少載入過程中重複的dom操作。
(2)善用 Vue 生命週期
善用 Vue 元件生命週期,在合適的 hook 去初始化資料,操作dom,能夠大幅提升載入體驗。
在mounted 階段,瀏覽器已經完成 dom 與 css 規則樹的 render,並完成 render tree佈局,這時候再去傳送資料請求,會拉長請求時間和渲染週期,所以建議在beforeCreate中執行,以此達到預渲染和請求的並行進行。
我們將活動初始化資料的動作放在 beforeCreate 階段,並將對 dom 的操作和監聽掛載在 mounted 中。
{ beforeCreate(){ fetch({ url: topicUrl, params: { //... } }).then(res=>{ //資料處理 //... }) }, mounted() { // global listener window.addEventListener('xxx'); // get dom element by refs this.$refs.xxx // get dom element use native api document.querySelector } }
對瀏覽器來說,整個渲染流程尚未開始或者說準備開始,對 vue 來說,例項尚未被初始化,data observer 和 event/watcher 也還未被呼叫,這個時候請求頁面初始化資料時機是比較成熟的。
(3)減少白屏時間
相比 Native 頁面,H5 頁面體驗問題主要是:開啟一個 H5 頁面需要做一系列處理,會有一段白屏時間,體驗糟糕。
白屏時間是指瀏覽器從響應使用者輸入網址地址,到瀏覽器開始顯示內容的時間。
本次專題優化,我們採用如下方式去減少白屏時間:
- 骨架屏,html直接渲染過渡效果
- 改造第三方 JS引入順序
- 使用 SplitChunksPlugin 拆分公共程式碼;
- 使用動態 import,切分頁面程式碼,減小首屏 JS 體積
其中改造骨架的方式是一種成本低,效果非常卓越的方式,更進階的方式有服務端直出等。由於悟空活動專題有快,靈的特點,配置改變需實時生效,所以前期我們權衡方案利弊,採用骨架,直接渲染過渡效果的方案。
頁面載入html後直接顯示載入效果,在底版本andriod手機中,webwiew初始化過程會有一個高度切換過程,載入後出現Native的titleBar,導致過渡效果會產生位置移動場景。
為了解決該問題,我們使用css3動畫來實現過渡效果延遲出現,避免與webview初始化衝突。
animation: loading 1s linear 300ms infinite; ··· @keyframes loading{ from { opacity: 1; } to { opacity: 1; } }
這一現象能側面反映出,loading出現基本於webview初始化同期進行,速度很快。為了解決loaidng瞬移的問題,我們採用純css3實現loading延遲出現,不與webview初始化衝突。
三、優化成果
1、同一專題優化前後資料對比
下述表格展示同一微元件和配置的活動在整體優化前後網站整體體驗評分,評分來自PageSpeed Insights。
國內活動 |
優化前 |
優化後 |
首次繪製 |
2.8s |
1.3s |
速度指數 |
4s |
3.8s |
繪製耗時 |
12s |
2.3s |
綜合得分(滿分 100) |
44 |
90 |
海外活動 |
優化前 |
優化後 |
首次繪製 |
3.5s |
1.3s |
速度指數 |
5.6s |
3.3s |
繪製耗時 |
3.5s |
2.8s |
綜合得分(滿分 100) |
67 |
92 |
2、國內活動效果
相同配置專題:
3、海外活動效果
相同配置專題:
四、效能資料收集
1、常用指標
關於指標,業界有非常多的方案和資料:
- 頁面載入時長
- 首屏載入時長
- Dom Ready 時長
- Dom Complete 時長
- 首頁渲染時長
- 首頁內容渲染時長
- 首頁有效渲染時長
- .......
基於活動的特點以及業務常關注點:我們對頁面白屏時間以及首次渲染時長以及一些個性化指標進行了收集,目的是統計活動專題載入時長,尋找優化空間。
2、如何計算
靜態資源的載入速度,可以利用 performance Timing API 取得
白屏時間:
白屏時間= 開始渲染時間(首位元組時間+HTML 下載完成時間)= responseStart - navigationStart
首次渲染時長= 全部事件註冊時長 = loadEventEnd - navigationStart
頁面繪製時間=獲取資料到載入結束 = loadEventEnd - fetchEnd(自行記錄)
3、上報方法
關於效能資料的上報方式,平臺使用 sendBeacon 進行無阻塞效能資料上報
navigator.sendBeacon()方法可用於通過HTTP將少量資料非同步傳輸到 Web 伺服器。
這個方法主要用於滿足統計和診斷程式碼的需要,傳送程式碼通常嘗試在解除安裝(unload)文件之前向 web 伺服器傳送資料。
function stat() { navigator.sendBeacon('/path', analyticsData) }點選並拖拽以移動
sendBeacon 發出的是非同步請求,請求作為瀏覽器任務執行,與當前頁面脫鉤。因此該方法不會阻塞頁面載入流程,也不會延遲頁面載入。
五、思考與展望
在上述探索的同時,我們同時在進行專題 SSR 、秒開、CSR的方案探索,不斷嘗試提升 H5 體驗的方式,追求卓越。
在筆者看來,效能優化不是一種手段,而是一種意識,開發者在實際開發過程中需要建立意識,在各處細節上去保證使用者體驗。
六、參考文獻
更多內容敬請關注vivo 網際網路技術微信公眾號
注:轉載文章請先與微訊號:Labs2020聯絡