本文首發於 vivo互聯網技術 微信公衆號
連接: https://mp.weixin.qq.com/s/6gtVR0nVNcZvREjwftZgzA
做者:悟空中臺研發團隊javascript
【悟空活動中臺】系列往期精彩文章:css
經過以前悟空活動中臺系列文章,你們對微組件、動態佈局等技術方案有了必定的瞭解。本篇咱們帶你們瞭解下悟空H5專題性能優化之路。html
在移動互聯網時代,H5頁面加載體驗相當重要。消費者行爲和觀念也會受到頁面加載時間的產生顯着影響,最明顯的就是咱們如今很難去等待一個頁面加載超過三秒的頁面,尤爲是年輕人。專一性能測試的SOASTA公司曾發表過結論:移動端加載每耗時1秒, 影響轉化率最高可達 20%。前端
在營銷中臺業務快速發展過程當中,悟空始終把網站響應速度和用戶體驗放在第一位,經過技術創新,不斷尋找最優加載方案,取得了很好的效果。下面咱們就一塊兒來探索下。vue
每談到性能優化,前端er就能聯想到一道經典面試題:從輸入URL到頁面加載,瀏覽器都執行了什麼?java
體驗優化的歷程和這道題同樣,須要系統化梳理、體系化實踐。咱們從網絡、資源、渲染、執行層出發,不斷探索加載優化方案。webpack
瀏覽器對網站第一次的域名 DNS 解析查找流程依次爲:瀏覽器緩存 >> 系統緩存 >> 路由器緩存 >> ISP DNS 緩存 >> 遞歸搜索。css3
移動端環境下,DNS 請求帶寬很是小,但延遲很高。針對該問題,咱們採起預讀取DNS方案,該方案能顯著下降延遲,平均加載時長可減小1秒左右。git
爲幫助瀏覽器對某些域名進行預解析,咱們對上線活動 html 文檔中新增 dns-prefetch標籤。加入該標籤後,瀏覽器解析步驟以下:github
第一步:用 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"> <% } %>
CDN 的全稱是 Content Delivery Network,即內容分發網絡。CDN 是構建在現有網絡基礎之上的智能虛擬網絡,依靠部署在各地的邊緣服務器,經過中心平臺的負載均衡、內容分發、調度等功能模塊,使用戶就近獲取所需內容,下降網絡擁塞,提升用戶訪問響應速度和命中率。
下圖展現終端用戶訪問頁面時,CDN獲取過程:
緩存對於CDN服務相當重要,合適的緩存策略可以下降源站的請求壓力,從而提高頁面加載速度,所以咱們須要優化靜態資源存儲方式和緩存策略。
CDN資源緩存配置以下:
悟空將H5專題的靜態資源上傳至CDN,帶來以下提高:
不緩存HTML入口文件的目的是防止客戶端緩存策略,致使主入口資源不更新,致使線上升級失敗。
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;
多路複用代替原有的序列以及阻塞機制,使得多個資源能夠在一個鏈接中並行下載,不受瀏覽器同一域名資源請求限制,提高整站的資源加載速度。
字體文件大小廣泛在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) }) }
圖片懶加載是一種很好的優化網頁或應用的方式,它可以在用戶滾動頁面時自動獲取更多的數據,新獲取的圖片不會影響到頁面呈現,同時視口外的圖片有可能永遠不須要被加載,可以極大的節約用戶流量以及服務器資源。'
懶加載的通常形式表現爲:
根據悟空現有的技術棧,咱們選擇vue-lazyload 去支撐位組件的圖片來加載:
悟空提供給組件開發者資源懶加載指令,用戶無需感知具體的加載邏輯,經過悟空的內置能力便可實現專題圖片懶加。具體用法以下:
<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>
在移動端環境下,圖片加載一直是須要重點優化的關鍵項,因此才延伸出懶加載這種交互方案來提升用戶體驗。
當該方案優化到了落地後,咱們下一步考慮如何在保證圖片質量的前提下,儘可能壓縮圖片體積,提高圖片加載效率。
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 壓縮時,也遇到種種問題,以下:
後續文章《悟空活動中臺 - 基於Webp的圖片高效加載方案》會詳細敘述悟空如何從平臺角度提供 Webp壓縮方案。
悟空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,修改成簡單請求,避免該請求發出預檢。
在網絡層以及資源壓縮優化落地後,接下來探索瀏覽器渲染執行優化點,涉及到瀏覽器,必定會聯想到網頁解析過程,下圖清晰的展現靜態資源如何經過瀏覽器最終顯示:
當dom元素變化會致使瀏覽器從新執行渲染樹生成、繪製,咱們稱之爲重排重繪。
什麼是重排?當 render tree 中的一部分(或所有)由於元素的規模尺寸,佈局,隱藏等改變而須要從新構建。這就稱爲重排(迴流)。每一個頁面至少須要一次迴流,就是在頁面第一次加載的時候。
瀏覽器結構示意圖:
能夠看到瀏覽器有負責解析、渲染請求內容的渲染引擎,哪些動做會致使瀏覽器重排:
(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操做。
善用 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 也還未被調用,這個時候請求頁面初始化數據時機是比較成熟的。
相比 Native 頁面,H5 頁面體驗問題主要是:打開一個 H5 頁面須要作一系列處理,會有一段白屏時間,體驗糟糕。
白屏時間是指瀏覽器從響應用戶輸入網址地址,到瀏覽器開始顯示內容的時間。
本次專題優化,咱們採用以下方式去減小白屏時間:
其中改造骨架的方式是一種成本低,效果很是卓越的方式,更進階的方式有服務端直出等。因爲悟空活動專題有快,靈的特色,配置改變需實時生效,因此前期咱們權衡方案利弊,採用骨架,直接渲染過渡效果的方案。
頁面加載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初始化衝突。
下述表格展現同一微組件和配置的活動在總體優化先後網站總體體驗評分,評分來自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 |
相同配置專題:
相同配置專題:
關於指標,業界有很是多的方案和數據:
基於活動的特色以及業務常關注點:咱們對頁面白屏時間以及首次渲染時長以及一些個性化指標進行了收集,目的是統計活動專題加載時長,尋找優化空間。
靜態資源的加載速度,能夠利用 performance Timing API 取得
白屏時間:
白屏時間 = 開始渲染時間(首字節時間+HTML 下載完成時間)= responseStart - navigationStart
首次渲染時長 = 所有事件註冊時長 = loadEventEnd - navigationStart
頁面繪製時間=獲取數據到加載結束 = loadEventEnd - fetchEnd(自行記錄)
關於性能數據的上報方式,平臺使用 sendBeacon 進行無阻塞性能數據上報
navigator.sendBeacon() 方法可用於經過HTTP將少許數據異步傳輸到 Web 服務器。
這個方法主要用於知足統計和診斷代碼的須要,發送代碼一般嘗試在卸載(unload)文檔以前向 web 服務器發送數據。
function stat() { navigator.sendBeacon('/path', analyticsData) }點擊並拖拽以移動
sendBeacon 發出的是異步請求,請求做爲瀏覽器任務執行,與當前頁面脫鉤。所以該方法不會阻塞頁面加載流程,也不會延遲頁面加載。
在上述探索的同時,咱們同時在進行專題 SSR 、秒開、CSR的方案探索,不斷嘗試提高 H5 體驗的方式,追求卓越。
在筆者看來,性能優化不是一種手段,而是一種意識,開發者在實際開發過程當中須要創建意識,在各處細節上去保證用戶體驗。
更多內容敬請關注 vivo 互聯網技術 微信公衆號
注:轉載文章請先與微信號:Labs2020 聯繫