前端性能优化 -- 从 10 多秒到 1.05 秒
關于 性能優化 是個大的面,這篇文章主要涉及到 前端 的幾個點,如 前端性能優化 的流程、常見技術手段、工具等。
提及 前端性能優化 ,大家應該都會想到 雅虎軍規,本文會結合 雅虎軍規 融入自己的了解知識,進行的總結和梳理 ?
詳情,可以查閱我的 博客 lishaoy.net
首先,我們先來看看 ? 雅虎軍規 的 35 條。
如對 雅虎軍規 的具體細則內容不是很了解,可自行去各搜索 ? 引擎 ,搜索 雅虎軍規 了解詳情。
壓縮 合并
對于 前端性能優化 自然要關注 首屏 打開速度,而這個速度,很大因素是花費在網絡請求上,那么怎么減少網絡請求的時間呢?
- 減少網絡請求次數
- 減小文件體積
- 使用 CDN 加速
所以 壓縮、合并 就是一個解決方案,當然可以用 gulp 、 webpack 、 grunt 等構建工具 壓縮、合并
JS、CSS 壓縮 合并
例如:gulp js、css 壓縮、合并代碼如下 ?
//壓縮、合并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 , ? 看看效果如何
如圖: 壓縮、合并 且放入 CND 之后的效果
以上是 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')); });圖片的合并可以采用 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 , ? 看看,效果如何
可見,請求時間是 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 信息,如: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 等中繼緩存服務器對其緩存。
協商緩存
協商緩存都是由瀏覽器和服務器協商,來確定是否緩存,協商主要通過下面兩組 header 字段,這兩組字段都是成對出現的,即第一次請求的響應頭帶上某個字段 ( Last-Modified 或者 Etag ) ,則后續請求會帶上對應的請求字段 (If-Modified-Since 或者 If-None-Match ) ,若響應頭沒有 Last-Modified 或者 Etag 字段,則請求頭也不會有對應的字段。
Last-Modified/If-Modified-Since
二者的值都是 GMT 格式的時間字符串,具體過程:
- 瀏覽器第一次跟服務器請求一個資源,服務器在返回這個資源的同時,在 respone 的 header 加上 Last-Modified 字段,這個 header 字段表示這個資源在服務器上的最后修改時間
- 瀏覽器再次跟服務器請求這個資源時,在 request 的 header 上加上 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-Modified 的 Header 在重新加載的時候會被更新,下次請求時,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 能做些什么:
- 后臺消息傳遞
- 網絡代理,轉發請求,偽造響應
- 離線緩存
- 消息推送
- … …
生命周期
service worker 初次安裝的生命周期,如圖 ?
從上 ? 圖可知,service worker 工作的流程:
監聽: 兩種狀態
- 終止以節省內存;
- 監聽獲取 fetch 和消息 message 事件。
現在,我們來寫個簡單的例子 ?
注冊 service worker
要安裝 service worker ,你需要在你的頁面上注冊它。這個步驟告訴瀏覽器你的 service worker 腳本在哪里。
if ('serviceWorker' in navigator) {navigator.serviceWorker.register('/sw.js').then(function(registration) {// Registration was successfulconsole.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 ,并決定哪些文件你想要緩存.
在我們的 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 responseif (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 可以看到,如圖
可以看到,大概有 30G ,我的站點只用了 183MB ,完全夠用了 ?
最后,來兩張圖
由于,文章篇幅過長,后續還會繼續總結 架構 方面的優化,例如
- bigpipe分塊輸出
- bigrender分塊渲染
- ...
以及,渲染 方面的優化,例如
- requestAnimationFrame
- well-change
- 硬件加速 GPU
- ...
以及,性能測試工具,例如
- PageSpeed
- audits
- ...
總結
以上是生活随笔為你收集整理的前端性能优化 -- 从 10 多秒到 1.05 秒的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自定义react-navigation的
- 下一篇: 手把手教你使用C#操作SQLite数据库