作者:子木;

https://segmentfault.com/a/1190000015052545

關於性能優化是個大的面,這篇文章主要涉及到前端的幾個點,如前端性能優化的流程、常見技術手段、工具等。

提及前端性能優化,大家應該都會想到雅虎軍規,本文會結合雅虎軍規融入自己的瞭解知識,進行的總結和梳理 。

詳情,可以查閱我的

雅虎軍規

首先,我們先來看看“雅虎軍規”的35條:

儘量減少 HTTP 請求個數——須權衡

使用CDN(內容分發網絡)

爲文件頭指定 Expires 或 Cache-Control ,使內容具有緩存性。

避免空的 src 和 href

使用 gzip 壓縮內容

把 CSS 放到頂部

把 JS 放到底部

避免使用 CSS 表達式

將 CSS 和 JS 放到外部文件中

減少 DNS 查找次數

精簡 CSS 和 JS

避免跳轉

剔除重複的 JS 和 CSS

配置 ETags

使 AJAX 可緩存

儘早刷新輸出緩衝

使用 GET 來完成 AJAX 請求

延遲加載

預加載

減少 DOM 元素個數

根據域名劃分頁面內容

儘量減少 iframe 的個數

避免 404

減少 Cookie 的大小

使用無 cookie 的域

減少 DOM 訪問

開發智能事件處理程序

用 代替 @import

避免使用濾鏡

優化圖像

優化 CSS Spirite

不要在 HTML 中縮放圖像——須權衡

favicon.ico要小而且可緩存

保持單個內容小於25K

打包組件成複合文本

如對雅虎軍規的具體細則內容不是很瞭解,可自行去各搜索引擎搜索雅虎軍規瞭解詳情。

壓縮 合併

對於前端性能優化自然要關注首屏打開速度,而這個速度,很大因素是花費在網絡請求上,那麼怎麼減少網絡請求的時間呢?

減少網絡請求次數

減小文件體積

使用CDN加速

所以壓縮、合併就是一個解決方案,當然可以用 gulp 、 webpack 、 grunt 等構建工具壓縮、合併。

JS、CSS 壓縮、合併

例如:gulp js、css 壓縮、合併代碼如下 :

//壓縮、合併js

gulp.task('scripts',function(){
returngulp.src([
'./public/lib/fastclick/lib/fastclick.min.js',
'./public/lib/jquery_lazyload/jquery.lazyload.js',
'./public/lib/velocity/velocity.min.js',
'./public/lib/velocity/velocity.ui.min.js',
'./public/lib/fancybox/source/jquery.fancybox.pack.js',
'./public/js/src/utils.js',
'./public/js/src/motion.js',
'./public/js/src/scrollspy.js',
'./public/js/src/post-details.js',
'./public/js/src/bootstrap.js',
'./public/js/src/push.js',
'./public/live2dw/js/perTips.js',
'./public/live2dw/lib/L2Dwidget.min.js',
'./public/js/src/love.js',
'./public/js/src/busuanzi.pure.mini.js',
'./public/js/src/activate-power-mode.js'
]).pipe(concat('all.js')).pipe(minify()).pipe(gulp.dest('./public/dist/'));
});
// 壓縮、合併 CSS
gulp.task('css',function(){
returngulp.src([
'./public/lib/font-awesome/css/font-awesome.min.css',
'./public/lib/fancybox/source/jquery.fancybox.css',
'./public/css/main.css',
'./public/css/lib.css',
'./public/live2dw/css/perTips.css'
]).pipe(concat('all.css')).pipe(minify()).pipe(gulp.dest('./public/dist/'));
});

然後,再把壓縮、合併的 JS、CSS 放入CDN,看看效果如何:


前端性能優化——從 10 多秒到 1.05 秒


前端性能優化——從 10 多秒到 1.05 秒


以上是 lishaoy.net 清除緩存後的首頁請求速度。

可見,請求時間是4.59 s,總請求個數51, 而js的請求個數是8,css的請求個數是3(其實就 all.css 一個,其它 2 個是 Google瀏覽器加載的), 而沒使用壓縮、合併時候,請求時間是10多秒,總請求個數有70多個,js的請求個數是20多個 ,對比請求時間性能提升1倍多。

如圖,有緩存下的首頁效果:


前端性能優化——從 10 多秒到 1.05 秒


基本都是秒開 。

Tips:在壓縮、合併後,單個文件控制在 25 ~ 30 KB左右,同一個域下,最好不要多於5個資源。

圖片壓縮、合併

例如: gulp 圖片壓縮代碼如下 :

//壓縮image
gulp.task('imagemin',function(){
gulp.src('./public/**/*.{png,jpg,gif,ico,jpeg}')
.pipe(imagemin())
.pipe(gulp.dest('./public'));
});

圖片的合並可以採用 CSSSpirite,方法就是把一些小圖用 PS 合成一張圖,用 css 定位顯示每張圖片的位置。

.top_right.phone{
background:url(../images/top_right.png)no-repeat7px-17px;
padding:038px;
}
.top_right.help{
background:url(../images/top_right.png)no-repeat0-47px;
padding:038px;
}

然後,把壓縮的圖片放入CDN ,看看效果如何:


前端性能優化——從 10 多秒到 1.05 秒


可見,請求時間是1.70 s,總請求個數50, 而img的請求個數是15(這裏因爲首頁都是大圖,就沒有合併,只是壓縮了) ,但是,效果很好 ,從4.59 s縮短到1.70 s, 性能又提升一倍。

再看看有緩存情況如何 :


前端性能優化——從 10 多秒到 1.05 秒


請求時間是1.05 s,有緩存和無緩存基本差不多。

Tips:大的圖片在不同終端,應該使用不同分辨率,而不應該使用縮放(百分比)

整個壓縮、合併(js、css、img) 再放入CDN,請求時間從10多秒 ,到最後的1.70 s,性能提升5倍多,可見,這個操作必要性。

緩存

緩存會根據請求保存輸出內容的副本,例如頁面、圖片、文件,當下一個請求來到的時候:如果是相同的URL,緩存直接使 用本地的副本響應訪問請求,而不是向源服務器再次發送請求。因此,可以從以下2個方面提升性能。

減少相應延遲,提升響應時間

減少網絡帶寬消耗,節省流量

我們用兩幅圖來了解下瀏覽器的緩存機制

1、瀏覽器第一次請求


前端性能優化——從 10 多秒到 1.05 秒


2、瀏覽器再次請求


前端性能優化——從 10 多秒到 1.05 秒


從以上兩幅圖中,可以清楚的瞭解瀏覽器緩存的過程:

首次訪問一個URL,沒有緩存,但是,服務器會響應一些header信息,如:expires、cache-control、last-modified、etag等,來記錄下次請求是否緩存、如何緩存。

再次訪問這個URL時候,瀏覽器會根據首次訪問返回的header信息,來決策是否緩存、如何緩存。

我們重點來分析下第二幅圖,其實是分兩條線路,如下 。

第一條線路:當瀏覽器再次訪問某個URL 時,會先獲取資源的 header 信息,判斷是否命中強緩存 (cache-control和expires) ,如命中,直接從緩存獲取資源,包括響應的 header信息 (請求不會和服務器通信) ,也就是強緩存,如圖:


前端性能優化——從 10 多秒到 1.05 秒


第二條線路:如沒有命中強緩存,瀏覽器會發送請求到服務器,請求會攜帶第一次請求返回的有關緩存的header 信息 (Last-Modified/If-Modified-Since和Etag/If-None-Match) ,由服務器根據請求中的相關 header 信息來比對結果是否協商緩存命中;若命中,則服務器返回新的響應 header 信息更新緩存中的對應 header信息,但是並不返回資源內容,它會告知瀏覽器可以直接從緩存獲取;否則返回最新的資源內容,也就是協商緩存

現在,我們瞭解到瀏覽器緩存機制分爲強緩存、協商緩存,再來看看他們的區別 :

緩存策略獲取資源形式狀態碼發送請求到服務器

強緩存從緩存取200(from memory cache)否,直接從緩存取

協商緩存從緩存取304(not modified)是,通過服務器來告知緩存是否可用

強緩存

與強緩存相關的 header 字段有兩個:

1、expires

expires:這是http1.0時的規範,它的值爲一個絕對時間的GMT格式的時間字符串,如Mon,10Jun201521:31:12GMT,如果發送請求的時間在expires之前,那麼本地緩存始終有效,否則就會發送請求到服務器來獲取資源。

2、cache-control

cache-control:max-age=number ,這是 http1.1 時出現的 header 信息,主要是利用該字段的 max-age值來進行判斷,它是一個相對值;資源第一次的請求時間和Cache-Control設定的有效期,計算出一個資源過期時間,再拿這個過期時間跟當前的請求時間比較,如果請求時間在過期時間之前,就能命中緩存,否則未命中,cache-control除了該字段外,還有下面幾個比較常用的設置值:

no-cache:不使用本地緩存。需要使用緩存協商,先與服務器確認返回的響應是否被更改,如果之前的響應中存在ETag,那麼請求的時候會與服務端驗證,如果資源未被更改,則可以避免重新下載。

no-store:直接禁止遊覽器緩存數據,每次用戶請求該資源,都會向服務器發送一個請求,每次都會下載完整的資源。

public:可以被所有的用戶緩存,包括終端用戶和CDN等中間代理服務器。

private:只能被終端用戶的瀏覽器緩存,不允許CDN等中繼緩存服務器對其緩存。

Tips:如果 cache-control 與 expires 同時存在的話,cache-control 的優先級高於 expires。

協商緩存

協商緩存都是由瀏覽器和服務器協商,來確定是否緩存,協商主要通過下面兩組 header字段,這兩組字段都是成對出現的,即第一次請求的響應頭帶上某個字段 (Last-Modified或者Etag) ,則後續請求會帶上對應的請求字段 (If-Modified-Since或者If-None-Match) ,若響應頭沒有Last-Modified或者Etag字段,則請求頭也不會有對應的字段。

1、Last-Modified/If-Modified-Since

二者的值都是 GMT 格式的時間字符串,具體過程:

瀏覽器第一次跟服務器請求一個資源,服務器在返回這個資源的同時,在respone的header加上Last-Modified字段,這個header字段表示這個資源在服務器上的最後修改時間。

瀏覽器再次跟服務器請求這個資源時,在request的header上加上If-Modified-Since字段,這個header字段的值就是上一次請求時返回的Last-Modified的值。

服務器再次收到資源請求時,根據瀏覽器傳過來If-Modified-Since和資源在服務器上的最後修改時間判斷資源是否有變化,如果沒有變化則返回304NotModified,但是不會返回資源內容;如果有變化,就正常返回資源內容。當服務器返回304NotModified的響應時,response header中不會再添加Last-Modified的header,因爲既然資源沒有變化,那麼Last-Modified也就不會改變,這是服務器返回304時的response header。

瀏覽器收到304的響應後,就會從緩存中加載資源。

如果協商緩存沒有命中,瀏覽器直接從服務器加載資源時,Last-Modified的Header在重新加載的時候會被更新,下次請求時,If-Modified-Since會啓用上次返回的Last-Modified值。

2、Etag/If-None-Match

這兩個值是由服務器生成的每個資源的唯一標識字符串,只要資源有變化就這個值就會改變;其判斷過程與Last-Modified、If-Modified-Since類似,與Last-Modified不一樣的是,當服務器返回304NotModified的響應時,由於ETag重新生成過,response header中還會把這個ETag返回,即使這個ETag跟之前的沒有變化。

Tips:Last-Modified與ETag是可以一起使用的,服務器會優先驗證ETag,一致的情況下,纔會繼續比對Last-Modified,最後才決定是否返回304。

Service Worker

1、什麼是 Service Worker

Service Worker本質上充當Web應用程序與瀏覽器之間的代理服務器,也可以在網絡可用時作爲瀏覽器和網絡間的代理。它們旨在(除其他之外)使得能夠創建有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採取適當的動作。他們還允許訪問推送通知和後臺同步API。

Service worker可以解決目前離線應用的問題,同時也可以做更多的事。Service Worker可以使你的應用先訪問本地緩存資源,所以在離線狀態時,在沒有通過網絡接收到更多的數據前,仍可以提供基本的功能(一般稱之爲 Offline First)。這是原生APP 本來就支持的功能,這也是相比於web app ,原生 app 更受青睞的主要原因。

再來看看 service worker能做些什麼:

後臺消息傳遞

網絡代理,轉發請求,僞造響應

離線緩存

消息推送

...

本文主要以(lishaoy.net)資源緩存爲例,闡述下 service worker如何工作。

2、生命週期

service worker初次安裝的生命週期,如圖 :


前端性能優化——從 10 多秒到 1.05 秒


從上 圖可知,service worker工作的流程:

安裝:service worker URL通過serviceWorkerContainer.register()來獲取和註冊。

激活:當service worker安裝完成後,會接收到一個激活事件(activate event)。onactivate主要用途是清理先前版本的service worker腳本中使用的資源。

監聽:兩種狀態

終止以節省內存;

監聽獲取 fetch 和消息 message 事件。

銷燬:是否銷燬由瀏覽器決定,如果一個service worker長期不使用或者機器內存有限,則可能會銷燬這個worker。

Tips:激活成功之後,在 Chrome 瀏覽器裏,可以訪問 chrome://inspect/#service-workers和 chrome://serviceworker-internals/ 可以查看到當前運行的service worker ,如圖 :


前端性能優化——從 10 多秒到 1.05 秒


現在,我們來寫個簡單的例子 。

3、註冊 service worker

要安裝 service worker ,你需要在你的頁面上註冊它。這個步驟告訴瀏覽器你的 service worker 腳本在哪裏。

if('serviceWorker'in navigator){
navigator.serviceWorker.register('/sw.js').then(function(registration){
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err){
// registration failed :(
console.log('ServiceWorker registration failed: ',err);
});
}

上面的代碼檢查 service worker API 是否可用,如果可用, service worker/sw.js 被註冊。如果這個 service worker 已經被註冊過,瀏覽器會自動忽略上面的代碼。

4、激活 service worker

在你的 service worker 註冊之後,瀏覽器會嘗試爲你的頁面或站點安裝並激活它。

install 事件會在安裝完成之後觸發。 install 事件一般是被用來填充你的瀏覽器的離線緩存能力。你需要爲 install 事件定義一個 callback ,並決定哪些文件你想要緩存.

// The files we want to cache
varCACHE_NAME='my-site-cache-v1';
varurlsToCache=[
'/',
'/css/main.css',
'/js/main.js'
];
self.addEventListener('install',function(event){
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache){
console.log('Opened cache');
returncache.addAll(urlsToCache);
})
);
});

在我們的 install callback 中,我們需要執行以下步驟:

開啓一個緩存

緩存我們的文件

決定是否所有的資源是否要被緩存

上面的代碼中,我們通過 caches.open 打開我們指定的 cache 文件名,然後我們調用 cache.addAll並傳入我們的文件數組。這是通過一連串 promise (caches.open 和 cache.addAll) 完成的。 event.waitUntil 拿到一個 promise 並使用它來獲得安裝耗費的時間以及是否安裝成功。

5、監聽 service worker

現在我們已經將你的站點資源緩存了,你需要告訴 service worker 讓它用這些緩存內容來做點什麼。有了 fetch 事件,這是很容易做到的。

每次任何被 service worker 控制的資源被請求到時,都會觸發 fetch 事件,我們可以給 service worker 添加一個 fetch 的事件監聽器,接着調用 event 上的 respondWith()方法來劫持我們的HTTP響應,然後你用可以用自己的方法來更新他們。

self.addEventListener('fetch',function(event){
event.respondWith(
caches.match(event.request);
);
});

caches.match(event.request) 允許我們對網絡請求的資源和 cache 裏可獲取的資源進行匹配,查看是否緩存中有相應的資源。這個匹配通過 url 和 vary header進行,就像正常的HTTP請求一樣。

那麼,我們如何返回 request 呢,下面 就是一個例子 :

self.addEventListener('fetch',function(event){
event.respondWith(
caches.match(event.request)
.then(function(response){
// Cache hit - return response
if(response){
returnresponse;
}
returnfetch(event.request);
}
)
);
});

上面的代碼裏我們定義了 fetch 事件,在 event.respondWith 裏,我們傳入了一個由 caches.match產生的 promise.caches.match 查找 request 中被 service worker 緩存命中的 response 。

如果我們有一個命中的 response ,我們返回被緩存的值,否則我們返回一個實時從網絡請求 fetch 的結果。

6、sw-toolbox

當然,我也可以使用第三方庫,例如:lishaoy.net 使用了sw-toolbox

sw-toolbox使用非常簡單,下面 就是 lishaoy.net 的一個例子 :

 "serviceWorker"in navigator ?navigator.serviceWorker.register('/sw.js').then(function(){
navigator.serviceWorker.controller?console.log("Assets cached by the controlling service worker."):console.log("Please reload this page to allow the service worker to handle network operations.")
}).catch(function(e){
console.log("ERROR: "+e)
}):console.log("Service workers are not supported in the current browser.")
以上是註冊一個service woker。
"use strict";
(function(){
varcacheVersion="20180527";
varstaticImageCacheName="image"+cacheVersion;
varstaticAssetsCacheName="assets"+cacheVersion;
varcontentCacheName="content"+cacheVersion;
varvendorCacheName="vendor"+cacheVersion;
varmaxEntries=100;
self.importScripts("/lib/sw-toolbox/sw-toolbox.js");
self.toolbox.options.debug=false;
self.toolbox.options.networkTimeoutSeconds=3;
self.toolbox.router.get("/images/(.*)",self.toolbox.cacheFirst,{
cache:{
name:staticImageCacheName,
maxEntries:maxEntries
}
});
self.toolbox.router.get('/js/(.*)',self.toolbox.cacheFirst,{
cache:{
name:staticAssetsCacheName,
maxEntries:maxEntries
}
});
self.toolbox.router.get('/css/(.*)',self.toolbox.cacheFirst,{
cache:{
name:staticAssetsCacheName,
maxEntries:maxEntries
}
......
self.addEventListener("install",function(event){
returnevent.waitUntil(self.skipWaiting())
});
self.addEventListener("activate",function(event){
returnevent.waitUntil(self.clients.claim())
})
})();

就這樣搞定了 (具體的用法可以去 https://googlechromelabs.github.io/sw-toolbox/api.html#main 查看)。

有的同學就問, service worker這麼好用,這個緩存空間到底是多大?其實,在Chrome可以看到,如圖:


前端性能優化——從 10 多秒到 1.05 秒


可以看到,大概有30G,我的站點只用了183MB,完全夠用了 。

最後,來兩張圖:


前端性能優化——從 10 多秒到 1.05 秒


前端性能優化——從 10 多秒到 1.05 秒


由於,文章篇幅過長,後續還會繼續總結架構方面的優化,例如:

bigpipe分塊輸出

bigrender分塊渲染

...

以及,渲染方面的優化,例如:

requestAnimationFrame

well-change

硬件加速 GPU

...

以及,性能測試工具,例如:

PageSpeed

audits

...

相关文章