做者:子木 segmentfault.com/a/1190000015052545css
關於 性能優化 是個大的面,這篇文章主要涉及到 前端 的幾個點,如 前端性能優化 的流程、常見技術手段、工具等。html
說起 前端性能優化 ,你們應該都會想到 雅虎軍規,本文會結合 雅虎軍規 融入本身的瞭解知識,進行的總結和梳理 。前端
詳情,能夠查閱個人博客:https://lishaoy.net。 jquery
首先,咱們先來看看"雅虎軍規"的35條:webpack
如對 雅虎軍規 的具體細則內容不是很瞭解,可自行去各搜索引擎搜索 雅虎軍規 瞭解詳情。git
對於 前端性能優化 天然要關注 首屏 打開速度,而這個速度,很大因素是花費在網絡請求上,那麼怎麼減小網絡請求的時間呢?github
因此壓縮、合併就是一個解決方案,固然能夠用 gulp 、 webpack 、 grunt 等構建工具壓縮、合併。 web
例如:gulp js、css 壓縮、合併代碼以下 :chrome
//壓縮、合併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,看看效果如何: gulp
以上是 lishaoy.net 清除緩存後的首頁請求速度。
可見,請求時間是 4.59 s ,總請求個數 51 , 而 js 的請求個數是 8 , css 的請求個數是 3 (其實就 all.css 一個,其它 2 個是 Google瀏覽器加載的), 而沒使用 壓縮、合併 時候,請求時間是 10 多秒,總請求個數有 70 多個, js 的請求個數是 20 多個 ,對比請求時間 性能 提高 1倍 多。
如圖,有緩存下的首頁效果:
基本都是秒開 。
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-repeat 7px -17px; padding: 0 38px; } .top_right .help { background: url(../images/top_right.png) no-repeat 0 -47px; padding: 0 38px; }
而後,把 壓縮 的圖片放入 CDN ,看看效果如何:
可見,請求時間是 1.70 s ,總請求個數 50 , 而 img 的請求個數是 15 (這裏由於首頁都是大圖,就沒有合併,只是壓縮了) ,可是,效果很好 ?? ,從 4.59 s 縮短到 1.70 s, 性能又提高一倍。
再看看有緩存狀況如何 :
請求時間是 1.05 s ,有緩存和無緩存基本差很少。
Tips:大的圖片在不一樣終端,應該使用不一樣分辨率,而不該該使用縮放(百分比)
整個 壓縮、合併 (js、css、img) 再放入 CDN ,請求時間從 10 多秒 ,到最後的 1.70 s,性能提高 5 倍多,可見,這個操做必要性。
緩存會根據請求保存輸出內容的副本,例如 頁面、圖片、文件,當下一個請求來到的時候:若是是相同的 URL,緩存直接使 用本地的副本響應訪問請求,而不是向源服務器再次發送請求。所以,能夠從如下 2 個方面提高性能。
咱們用兩幅圖來了解下瀏覽器的 緩存機制。
一、瀏覽器第一次請求
二、瀏覽器再次請求
從以上兩幅圖中,能夠清楚的瞭解瀏覽器 緩存 的過程:
咱們重點來分析下第二幅圖,實際上是分兩條線路,以下 。
第一條線路: 當瀏覽器再次訪問某個 URL 時,會先獲取資源的 header 信息,判斷是否命中強緩存 (cache-control和expires) ,如命中,直接從緩存獲取資源,包括響應的 header信息 (請求不會和服務器通訊) ,也就是 強緩存 ,如圖:
第二條線路: 如沒有命中 強緩存 ,瀏覽器會發送請求到服務器,請求會攜帶第一次請求返回的有關緩存的 header 信息 (Last-Modified/If-Modified-Since和Etag/If-None-Match) ,由服務器根據請求中的相關 header 信息來比對結果是否協商緩存命中;若命中,則服務器返回新的響應 header 信息更新緩存中的對應 header 信息,可是並不返回資源內容,它會告知瀏覽器能夠直接從緩存獲取;不然返回最新的資源內容,也就是 協商緩存。
如今,咱們瞭解到瀏覽器緩存機制分爲 強緩存、協商緩存,再來看看他們的區別 :
與強緩存相關的 header 字段有兩個:
一、expires
expires: 這是 http1.0 時的規範,它的值爲一個絕對時間的 GMT 格式的時間字符串,如 Mon,10Jun201521:31:12GMT ,若是發送請求的時間在 expires 以前,那麼本地緩存始終有效,不然就會發送請求到服務器來獲取資源。
二、cache-control
cache-control: max-age=number ,這是 http1.1 時出現的 header 信息,主要是利用該字段的 max-age 值來進行判斷,它是一個相對值;資源第一次的請求時間和 Cache-Control 設定的有效期,計算出一個資源過時時間,再拿這個過時時間跟當前的請求時間比較,若是請求時間在過時時間以前,就能命中緩存,不然未命中, cache-control 除了該字段外,還有下面幾個比較經常使用的設置值:
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 格式的時間字符串,具體過程:
二、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
Service Worker 本質上充當Web應用程序與瀏覽器之間的代理服務器,也能夠在網絡可用時做爲瀏覽器和網絡間的代理。它們旨在(除其餘以外)使得可以建立有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採起適當的動做。他們還容許訪問推送通知和後臺同步API。
Service worker 能夠解決目前離線應用的問題,同時也能夠作更多的事。 Service Worker 可使你的應用先訪問本地緩存資源,因此在離線狀態時,在沒有經過網絡接收到更多的數據前,仍能夠提供基本的功能(通常稱之爲 Offline First)。這是原生APP 原本就支持的功能,這也是相比於 web app ,原生 app 更受青睞的主要緣由。
再來看看 service worker 能作些什麼:
本文主要以(lishaoy.net)資源緩存爲例,闡述下 service worker如何工做。
二、生命週期
service worker 初次安裝的生命週期,如圖 :
從上 圖可知,service worker 工做的流程:
Tips:激活成功以後,在 Chrome 瀏覽器裏,能夠訪問 chrome://inspect/#service-workers和 chrome://serviceworker-internals/ 能夠查看到當前運行的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 裏可獲取的資源進行匹配,查看是否緩存中有相應的資源。這個匹配經過 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) { 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()) }) })();
就這樣搞定了 (具體的用法能夠去 https://googlechromelabs.github.io/sw-toolbox/api.html#main 查看)。
有的同窗就問, service worker 這麼好用,這個緩存空間究竟是多大?其實,在 Chrome能夠看到,如圖:
能夠看到,大概有 30G ,個人站點只用了 183MB ,徹底夠用了 。
最後,來兩張圖:
因爲,文章篇幅過長,後續還會繼續總結 架構 方面的優化,例如:
以及,渲染 方面的優化,例如:
以及,性能測試工具,例如: