[轉] 前端性能優化--從 10 多秒到 1.05 秒

首先,咱們先來看看 👀 雅虎軍規35 條。javascript

  1. 儘可能減小 HTTP 請求個數——須權衡
  2. 使用 CDN(內容分發網絡)
  3. 爲文件頭指定 Expires 或 Cache-Control ,使內容具備緩存性。
  4. 避免空的 src 和 href
  5. 使用 gzip 壓縮內容
  6. 把 CSS 放到頂部
  7. 把 JS 放到底部
  8. 避免使用 CSS 表達式
  9. 將 CSS 和 JS 放到外部文件中
  10. 減小 DNS 查找次數
  11. 精簡 CSS 和 JS
  12. 避免跳轉
  13. 剔除重複的 JS 和 CSS
  14. 配置 ETags
  15. 使 AJAX 可緩存
  16. 儘早刷新輸出緩衝
  17. 使用 GET 來完成 AJAX 請求
  18. 延遲加載
  19. 預加載
  20. 減小 DOM 元素個數
  21. 根據域名劃分頁面內容
  22. 儘可能減小 iframe 的個數
  23. 避免 404
  24. 減小 Cookie 的大小
  25. 使用無 cookie 的域
  26. 減小 DOM 訪問
  27. 開發智能事件處理程序
  28. 用 代替 @import
  29. 避免使用濾鏡
  30. 優化圖像
  31. 優化 CSS Spirite
  32. 不要在 HTML 中縮放圖像——須權衡
  33. favicon.ico要小並且可緩存
  34. 保持單個內容小於25K
  35. 打包組件成複合文本

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

壓縮 合併

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

  • 減小網絡請求次數
  • 減少文件體積
  • 使用 CDN 加速

因此 壓縮、合併 就是一個解決方案,固然能夠用 gulpwebpackgrunt 等構建工具 壓縮、合併前端

JS、CSS 壓縮 合併

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

//壓縮、合併js gulp.task('scripts', function () { return gulp.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 () { return gulp.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 , 👀 看看效果如何jquery

如圖:* 壓縮、合併 且放入 CND 以後的效果 *webpack

首頁請求速度(js) 首頁請求速度(css)

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

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

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

首頁請求速度(緩存)

基本都是秒開 😝

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')); }); 

圖片的合併能夠採用 CSS Spirite,方法就是把一些小圖用 PS 合成一張圖,用 css 定位顯示每張圖片的位置

.top_right .phone { background: url(../images/top_right.png) no-repeat 7px -17px; padding: 0 38px; } .top_right .help { background: url(../images/top_right.png) no-repeat 0 -47px; padding: 0 38px; } 

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

首頁請求速度(images)

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

再看看有緩存狀況如何 😏

首頁請求速度(images 緩存)

請求時間是 1.05 s ,有緩存和無緩存基本差很少

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

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

緩存

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

  • 減小相應延遲,提高響應時間
  • 減小網絡帶寬消耗,節省流量

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

瀏覽器第一次請求

 

no-shadow第一次請求

 

瀏覽器再次請求

 

no-shadow再次請求

 

從以上兩幅圖中,能夠清楚的瞭解瀏覽器 緩存 的過程。 首次訪問一個 URL ,沒有 緩存 ,可是,服務器會響應一些 header 信息,如:expires、cache-control、last-modified、etag 等,來記錄下次請求是否緩存、如何緩存。 再次訪問這個 URL 時候,瀏覽器會根據首次訪問返回的 header 信息,來決策是否緩存、如何緩存。 咱們重點來分析下第二幅圖,實際上是分兩條線路,以下 👇

  • 第一條線路: 當瀏覽器再次訪問某個 URL 時,會先獲取資源的 header 信息,判斷是否命中強緩存 (cache-control和expires) ,如命中,直接從緩存獲取資源,包括響應的 header 信息 (請求不會和服務器通訊) ,也就是 強緩存 ,如圖
強緩存
  • 第二條線路: 如沒有命中 強緩存 ,瀏覽器會發送請求到服務器,請求會攜帶第一次請求返回的有關緩存的 header 信息 (Last-Modified/If-Modified-Since和Etag/If-None-Match) ,由服務器根據請求中的相關 header 信息來比對結果是否協商緩存命中;若命中,則服務器返回新的響應 header 信息更新緩存中的對應 header 信息,可是並不返回資源內容,它會告知瀏覽器能夠直接從緩存獲取;不然返回最新的資源內容,也就是 協商緩存

如今,咱們瞭解到瀏覽器緩存機制分爲 強緩存、協商緩存,再來看看他們的區別 👇

緩存策略 獲取資源形式 狀態碼 發送請求到服務器
強緩存 從緩存取 200(from memory cache) 否,直接從緩存取
協商緩存 從緩存取 304(not modified) 是,經過服務器來告知緩存是否可用

強緩存

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

expires

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

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 字段,則請求頭也不會有對應的字段。

Last-Modified/If-Modified-Since

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

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

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

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

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

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

Etag/If-None-Match

這兩個值是由服務器生成的每一個資源的惟一標識字符串,只要資源有變化就這個值就會改變;其判斷過程與 Last-Modified、If-Modified-Since 相似,與 Last-Modified 不同的是,當服務器返回 304 Not Modified 的響應時,因爲 ETag 從新生成過,response header 中還會把這個 ETag 返回,即便這個 ETag 跟以前的沒有變化。

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

Service Worker

什麼是 Service Worker

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

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

再來看看 👀 service worker 能作些什麼:

  • 後臺消息傳遞
  • 網絡代理,轉發請求,僞造響應
  • 離線緩存
  • 消息推送
  • … …

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

生命週期

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

 

no-shadowsw生命週期

 

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

  1. 安裝: service worker URL 經過 serviceWorkerContainer.register() 來獲取和註冊。
  2. 激活:service worker 安裝完成後,會接收到一個激活事件(activate event)。 onactivate 主要用途是清理先前版本的 service worker 腳本中使用的資源。
  3. 監聽: 兩種狀態
    • 終止以節省內存;
    • 監聽獲取 fetch 和消息 message 事件。
  4. 銷燬: 是否銷燬由瀏覽器決定,若是一個 service worker 長期不使用或者機器內存有限,則可能會銷燬這個 worker

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

service worker

如今,咱們來寫個簡單的例子 🌰

註冊 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 已經被註冊過,瀏覽器會自動忽略上面的代碼。

激活 service worker

在你的 service worker 註冊以後,瀏覽器會嘗試爲你的頁面或站點安裝並激活它。 install 事件會在安裝完成以後觸發。install 事件通常是被用來填充你的瀏覽器的離線緩存能力。你須要爲 install 事件定義一個 callback ,並決定哪些文件你想要緩存.

// The files we want to cache var CACHE_NAME = 'my-site-cache-v1'; var urlsToCache = [ '/', '/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'); return cache.addAll(urlsToCache); }) ); }); 

在咱們的 install callback 中,咱們須要執行如下步驟:

  • 開啓一個緩存
  • 緩存咱們的文件
  • 決定是否全部的資源是否要被緩存

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

監聽 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 裏可獲取的資源進行匹配,查看是否緩存中有相應的資源。這個匹配經過 urlvary header 進行,就像正常的 HTTP 請求同樣。

那麼,咱們如何返回 request 呢,下面 👇 就是一個例子 🌰

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

上面的代碼裏咱們定義了 fetch 事件,在 event.respondWith 裏,咱們傳入了一個由 caches.match 產生的 promise.caches.match 查找 request 中被 service worker 緩存命中的 response 。 若是咱們有一個命中的 response ,咱們返回被緩存的值,不然咱們返回一個實時從網絡請求 fetch 的結果。

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 () { var cacheVersion = "20180527"; var staticImageCacheName = "image" + cacheVersion; var staticAssetsCacheName = "assets" + cacheVersion; var contentCacheName = "content" + cacheVersion; var vendorCacheName = "vendor" + cacheVersion; var maxEntries = 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) { return event.waitUntil(self.skipWaiting()) }); self.addEventListener("activate", function (event) { return event.waitUntil(self.clients.claim()) }) })(); 

就這樣搞定了 🍉 (具體的用法能夠去 sw-toolbox 查看)

有的同窗就問,service worker 這麼好用,這個緩存空間究竟是多大?其實,在 Chrome 能夠看到,如圖

fstorage quota

能夠看到,大概有 30G ,個人站點只用了 183MB ,徹底夠用了 🍓

最後,來兩張圖

from ServiceWorker Cache Storage

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

  • bigpipe分塊輸出
  • bigrender分塊渲染
  • ...

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

  • requestAnimationFrame
  • well-change
  • 硬件加速 GPU
  • ...

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

  • PageSpeed
  • audits
  • ...
相關文章
相關標籤/搜索