這幾種解決方案讓你的首屏加載快到起飛!
首屏加載的意義不言而喻,畢竟第一印象最重要,直接影響用戶體驗和留存。當用戶使用你的產(chǎn)品的時候,一上來半天刷不出首頁,很多用戶往往就直接給你Ctrl+F4了。
那么問題來了,怎么做首屏優(yōu)化。在了解怎么優(yōu)化之前,我們需要知道首屏加載的幾個重要時刻。
圖片
- 首次加載
- 什么時候加載出頁面
- 什么時候用戶可以交互
為此,我們可以從以下幾個方面來進行相關的優(yōu)化。
資源體積太大
資源壓縮與合并/代碼拆分
將小圖片內(nèi)聯(lián)為Data URL,也可以額減小HTTP的請求數(shù)量,需要注意的是,瀏覽器緩存并不會存儲Data URL格式的圖片,放在css的background-image屬性中即可。由于使用Data URL在渲染和CPU消耗上更大,所以使用時也需要謹慎而不應該一味的濫用。
通過以上幾種方法,我們主要要解決的問題是以下幾個
- 減少HTTP請求數(shù)量
- 減少請求資源的大小
- 減少不必要的代碼
需要注意的是,在CSS和JS合并的時候我們需要謹慎,并非所有CSS和JS合并都是好的,不能一味的為了做首屏或者性能優(yōu)化而引發(fā)了其他方面的問題。在有若干個小文件的時候,或者是沒有沖突的同模塊的文件的時候是可以考慮合并的。但是如果我們把其他不同模塊的CSS和JS也合并到了一起,可能會給后續(xù)的解析處理和自己的代碼維護帶來問題,而且JS文件間還可能會出現(xiàn)命名空間的沖突。這些都是無腦的資源合并會帶來的問題。
傳輸壓縮
- Gzip ??需要注意的是,太小的文件啟用Gzip以后可能反而會增大,這是因為壓縮前會寫入一段映射字典,但是這個是Byte層面的可以忽略不計。
HTTP2.0和HTTP1.1
圖片
3630394917-5b229aa26f852_fix732.png
圖上簡單概括就是:
- HTTP1.1的keep-alive默認開啟,而且keep-alive是按順序請求返回,等到上一個請求返回以后才會進行下一個請求,會有阻塞問題。keep-alive的請求順序如下:
- 1.請求style.css
- 2.返回style.css
- 3.請求script.js
- 4.返回script.js ??如何判斷是否開啟了keep-alive可以從Response Headers看到,切記是HTTP1.1中的。
圖片
- 但是HTTP2.0就不一樣了,HTTP2.0的多路復用可以一次性發(fā)送多個請求,不一定是按順序也不需要等待上一個請求返回。這些請求都有唯一標識,所以可以無序。
比較詳細的內(nèi)容在面試點之《HTTP協(xié)議與TCP/IP協(xié)議》[1]中有。
HTTP緩存
在此之前,我們要熟悉兩個概念,強緩存和協(xié)商緩存。
圖片
強緩存:瀏覽器直接從本地緩存中取數(shù)據(jù),而不用去請求服務器。Expires/Cache-Control(優(yōu)先級更高,且為通用字段,請求和返回報文中都可以使用)
圖片
- Cache-Control常見的屬性有很多:
- max-age:單位是秒,意為緩存的持續(xù)時間(壽命)。比如Cache-Control:max-age=60;意為60秒內(nèi)會直接使用該緩存,而max-age<=0時則每次都要發(fā)送請求驗證緩存是否有修改,改了的話返回200,沒改返回304;
- no-store:不使用緩存,直接去服務器請求數(shù)據(jù);
- no-cache:不緩存過期資源;客戶端會要求緩存服務器不要從緩存拿數(shù)據(jù),而是去請求源服務器,源服務器會告訴緩存服務器,使用緩存時先告訴我驗證一下。區(qū)別在于直接使用緩存是不會請求源服務器的。
- public:表明響應可以被任何對象(發(fā)送請求的客戶端,代理服務器等等)緩存;
- privite:表明響應只能被單個用戶緩存,不能作為共享緩存(即代理服務器不能緩存它);
- Expires:GMT格式的時間字段,意為到期時間。超過此時間以后資源作廢。缺點在于是和本地時間比較,本地時間可能存在誤差。也可以設置為0或者負數(shù),此時不使用緩存直接從服務器重新請求數(shù)據(jù)。
協(xié)商緩存:瀏覽器發(fā)送請求到服務器,服務器判斷是否使用本地緩存。
Etag(響應頭中)和If-None-Match(請求頭中)【優(yōu)先級更高】
圖片
??兩者的值都是該資源的唯一標識字符串。第一次請求時,響應頭(Response Headers)中會添加一個Etag的字段,再次請求服務器時,會在請求頭報文(Request Headers)中添加If-None-Match字段,它的值就是上次響應頭(Response Headers)中的Etag值。服務器會比較兩個值是否相同。
- 相同: 返回狀態(tài)碼304 Not Modified,直接取用本地緩存即可。
- 不相同: 說明資源內(nèi)容已經(jīng)發(fā)生了變化,此時接受請求,返回最新的數(shù)據(jù),并更新Etag值。Etag中可能會有W/這樣的標識,W/(大小寫敏感) 表示使用弱驗證器。弱驗證器很容易生成,但不利于比較。強驗證器是比較的理想選擇,但很難有效地生成。相同資源的兩個弱Etag值可能語義等同,但不是每個字節(jié)都相同。
需要特別注意的是,Etag變化并不代表文件內(nèi)容一定變化,Etag的值取決于服務端的算法。比如Etag的值由最后一次修改時間+內(nèi)容經(jīng)過哈希算法而得,但是此時我編輯了內(nèi)容并沒有修改內(nèi)容,最后一次的修改時間會發(fā)現(xiàn)變化,此時的Etag值肯定也會變化,但是內(nèi)容并沒有發(fā)生改變
Last-Modified(響應頭中)和If-Modified-Since(請求頭中)
圖片
??如圖所示,兩者的值都是GMT格式的時間字符串,Last-Modified意為最后一次修改文件的時間,其實這個方法和上一種原理一樣,只不過一個是用唯一標識字符串來比較,一個是用最后一次修改的時間來比較。也是第一次請求時,響應頭(Response Headers)中會添加一個Last-Modified的字段,記錄了最后一次文件修改的時間,然后再次請求時,會在請求頭(Request Headers)中添加If-Modified-Since字段,值就是上一次響應時響應頭中Last-Modified的值。服務器會比較Last-Modefied和Last-Modefied-Since的值。
- 相同: 返回狀態(tài)碼304 Not Modified,直接取用本地緩存即可。
- 不相同: 說明資源內(nèi)容已經(jīng)發(fā)生了變化,此時接受請求,返回最新的數(shù)據(jù),并更新Last-Modified值。
兩者的區(qū)別
- 當一個文件周期性修改而文件內(nèi)容并沒有修改的時候,Last-Modified還是會從服務器請求新的數(shù)據(jù),這并不是我們希望看到的,這個時候Etag能更好的處理這種情況。
- Last-Modified的精準度只有秒,當一秒內(nèi)修改多次時,這種修改無法判斷。??所以Etag的精度和適用性要強于Last-Modefied,兩者雖然可以同時使用,但是服務器會優(yōu)先驗證Etag。
首次進入白屏時間過長
骨架屏(vue-content-placeholders)
效果如下圖:(圖片來源網(wǎng)絡,侵刪)
圖片
我使用的是這個插件vue-content-placeholders[2],其次,骨架屏是需要自己畫的,你需要把布局畫好做成一個組件,在需要的頁面引用,然后等數(shù)據(jù)請求回來以后隱藏掉再顯示正常的頁面就可以。通常僅僅在接口請求比較多的頁面用到骨架屏,畢竟當你的頁面改動的時候骨架屏的頁面布局也需要改動,如果每個頁面都使用骨架屏未免太浪費開發(fā)時間也增加了日后的維護成本。
- root <content-placeholders>(以下三個屬性會應用到所有子組件中)
- animated (default: true; type: Boolean):是否開啟動畫效果
- rounded (default: false; type: Boolean):是否加上邊角度,等同于設置border-radius
- centered (default: false; type: Boolean):是否同時垂直+水平居中顯示
- <content-placeholders-heading />
- img (default: false; type: Boolean):是否顯示圖片模塊
圖片
- <content-placeholders-text />
- lines (default: 4; type: Boolean):顯示幾條
圖片
- <content-placeholders-img />
圖片
可以在標簽上直接設置class和style
<content-placeholders class='xxx' :animated='true' :rounded='true' :centered='true'>
<content-placeholders-heading class='xxx' style='xxxx' :img="true" />
</content-placeholders>
<content-placeholders style='xxxx' :animated='true' :rounded='true' :centered='true'>
<content-placeholders-text class='xxx' style='xxxx' :lines="3" />
<content-placeholders-img class='xxx' style='xxxx' />
</content-placeholders>
vue文件引入需要的骨架屏組件即可。
<template>
<div>
<Placeholders-Component v-if="isShow"></Placeholders-Component>
<div v-else class="main">
·······
</div>
</div>
</template>
<script>
import PlaceholdersComponent from "./PlaceholdersComponent";//引入寫好的骨架屏組件
export default {
components: {
PlaceholdersComponent,
},
data() {
return {
hidden: true,
};
},
};
</script>
懶加載
- Vue的路由懶加載(官方文檔[3]) ??在我們的項目構(gòu)建加載時,JavaScript文件才是最影響加載時長的,此時我們把各個路由切分成塊,按需加載路由,這樣會節(jié)約相當多的時間。
使用方法也很簡單:
const Foo = () => import('./Foo.vue')
const Bar = () => import('./Bar.vue')
const router = new VueRouter({
routes: [{
path: '/foo',
component: Foo
},{
path: '/bar',
component: Bar
}]
})
- 圖片懶加載 ??在我的《前端性能優(yōu)化(二):圖片[4]》文中已經(jīng)講了比較詳細的圖片懶加載在此就不多解釋了。
預渲染
我使用的是Vue.js官網(wǎng)介紹使用的prerender-spa-plugin。這個插件的配置很多,它的GitHub地址在這里[5]。
Vue.js的官網(wǎng)中給到該預渲染插件如下描述:
圖片
預渲染也有一些需要注意的點:
- 如果路由過多,成百上千(當然這應該是極少數(shù)情況)的時候,使用預渲染會非常緩慢。雖然說每次更新只用執(zhí)行一次,但是需要的時間會非常長。
- 接口請求過多的時候不建議使用預渲染,而是使用SSR;如果沒有SEO的需求,骨架屏是個更簡單方便的選擇,不需過多的配置更不需要后臺配合。
- 預渲染不執(zhí)行JavaScript,只試用于純靜態(tài)頁面,當然你也可以配置等接口請求返回拿到數(shù)據(jù)以后再預渲染,這種僅僅適用于少量接口請求。而SSR比預渲染的不同就是多了一步執(zhí)行JavaScript。
安裝
npm i prerender-spa-plugin -D
webpack.prod.conf.js
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
const webpackConfig = merge(baseWebpackConfig, {
plugins:[
new PrerenderSPAPlugin({
// 必需 - 要預渲染的 webpack 輸出應用程序的路徑。
staticDir: path.join(__dirname, '../dist'),
// 必需 - 要渲染的路由
routes: ['/', '/about'],
renderer: new Renderer({
inject: {
foo: 'bar'
},
// 渲染時顯示瀏覽器窗口。用于調(diào)試。false意為打開,true是不打開
headless: false,
// 可選 - 等待渲染,直到在文檔上調(diào)度指定的事件。
// 例如,使用 `document.dispatchEvent(new Event('event-home'))`
renderAfterDocumentEvent: 'event-home'
})
}),
]
})
main.js
new Vue({
el: '#app',
store,
router,
components: { App },
template: '<App/>',
mounted() {
document.dispatchEvent(new Event('event-home'))
}
})
npm run build以后我們會得到如下的文件,這個時候我們會發(fā)現(xiàn),是多了一個about的文件夾。這是由于配置了new PrerenderSPAPlugin中的routes。
圖片
圖片
而且此時我們會發(fā)現(xiàn),在打包好的index.html文件中,id為app的div標簽中有其他標簽內(nèi)容。倘若我們不使用預渲染,打包出的文件里,這個標簽里是空的不會有任何標簽。
圖片
圖片
SSR(服務端渲染)
- 分頁首屏加載
- 各好的SEO
- 搜索排名很重要的時候
- 大型架構(gòu),動態(tài)頁面,且面對公眾用戶
具體使用可參考Vue的官方文檔
Service workers
Service workers可以很好的解決離線時候的首頁加載問題。但是鑒于文章長度限制,就在此處不細說了。
優(yōu)化資源加載的順序
某乎有位大佬說的很詳細,鏈接在此[6]。
prefetch
主要加載較晚才會用到的資源,告知瀏覽器后,瀏覽器就會在閑時去加載對應的資源,很適合在懶加載時使用。
<link rel="prefetch" href="xxxxx.css">
對于使用prefetch獲取資源,其優(yōu)先級默認為最低Lowest,可以認為當瀏覽器空閑的時候才會去獲取的資源。
圖片
preload
主要用來加載當前頁面很重要的資源。
<link rel="preload" href="xxxxx.png" as="image">
瀏覽器通過as值能得知資源類型,還能根據(jù)as的值發(fā)送適當?shù)腁ccept頭部信息。甚至可以通過as來標識他們請求資源的優(yōu)先級(比如說使用as="style"屬性將獲得最高的優(yōu)先級,即使資源不是樣式文件)
圖片
兩者的異同
- 如果關閉了瀏覽器,倘若請求還沒有結(jié)束,preload會立刻結(jié)束請求,但是prefetch會繼續(xù)請求。
- 如果preload還未下載完就已經(jīng)開始解析所需資源,此時并不會二次請求,而是等待此次請求完畢。
- 倘若對同一個資源同時使用preload和prefetch則會導致二次加載。
- preload是告訴瀏覽器頁面必定需要的資源,瀏覽器一定會加載這些資源,而prefetch是告訴瀏覽器頁面可能需要的資源,瀏覽器不一定會加載這些資源。
補充
關于Vue中的一些優(yōu)化(并非首屏優(yōu)化),我推薦黃軼老師的這篇文章《揭秘 Vue.js 九個性能優(yōu)化技巧》[7]。
在我個人理解看來,性能優(yōu)化的最終目的并不是完全追求時間上的長短,核心目的是給用戶更好的體驗,在提升了幀數(shù)的情況舍棄一點點加載或者渲染時間在整體體驗上要比完全追求數(shù)值上的長短有意義的多。也就是上面文章中這位朋友分享的觀點:
圖片