本文從提高資源請求速度,資源壓縮、緩存、渲染等多種角度出發,尋找悟空活動專題加載優化方案。
本文首發于 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擷取過程:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbwxCdh1mcvZ2LcV2Zh1Wa9M3clN2byBXLzN3btg3P3pVdC5GTy0EROVTSEpVbkRkWyUFVahmVE9UeRpWWtZ0RPJTSH9EbOpWTzMGROBTStlVNZdFT3Z1MMBjVtJmaONjY2FFWaVXNTlVdsdUYq50MiV3YXJGcOJzY2lTeMZTTINGMShUYvwlbj5yZtlmbkN3YuQnclZnbvN2Ztl2Lc9CX6MHc0RHaiojIsJye.jpg)
緩存對于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 進行有損壓縮後,我們大概可以将原本的圖檔大小壓縮至原來的十分之一左右,而圖檔品質卻沒有大的損失。這确實是一個驚人的效率。
我們可以看下一組資料來看下 webp 有損壓縮效果:
Webp 有損壓縮(75%品質比)
await execFileSync(cwebp, ['-q', '75', filePath, '-o', webpPath]);
原大小 | 壓縮時間(ms) | 壓縮後大小 |
999kb | 237 | 38kb |
221 | ||
228 | ||
261 |
在轉換結束後,悟空會将原圖檔和轉換後的 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 | ||
5.6s | 3.3s | |
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 體驗的方式,追求卓越。
在筆者看來,性能優化不是一種手段,而是一種意識,開發者在實際開發過程中需要建立意識,在各處細節上去保證使用者體驗。
六、參考文獻
- https://developers.google.com/web/fundamentals/performance/http2?hl=zh-cn
- https://juejin.im/entry/56ce7d1a1532bc005372a7fa
更多内容敬請關注 vivo 網際網路技術 微信公衆号
注:轉載文章請先與微信号:Labs2020 聯系
分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。