本文首發於 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 聯系