移動互聯網時代,用戶對於網頁的打開速度要求愈來愈高。首屏做爲直面用戶的第一屏,其重要性不言而喻。優化用戶體驗更是咱們前端開發很是須要 focus 的東西之一。javascript
從用戶的角度而言,當打開一個網頁,每每關心的是從輸入完網頁地址後到最後展示完整頁面這個過程須要的時間,這個時間越短,用戶體驗越好。因此做爲網頁的開發者,就從輸入url到頁面渲染呈現這個過程當中去提高網頁的性能。css
因此輸入URL後發生了什麼呢?在瀏覽器中輸入url會經歷域名解析、創建TCP鏈接、發送http請求、資源解析等步驟。html
http緩存優化是網頁性能優化的重要一環,這一部分我會在後續筆記中作一個詳細總結,因此本文暫很少作詳細整理。本文主要從網頁渲染過程、網頁交互以及Vue應用優化三個角度對性能優化作一個小結。前端
首先談談拿到服務端資源後瀏覽器渲染的流程:
vue
1. 解析 HTML 文件,構建 DOM 樹,同時瀏覽器主進程負責下載 CSS 文件 2. CSS 文件下載完成,解析 CSS 文件成樹形的數據結構,而後結合 DOM 樹合併成 RenderObject 樹 3. 佈局 RenderObject 樹 (Layout/reflow),負責 RenderObject 樹中的元素的尺寸,位置等計算 4. 繪製 RenderObject 樹 (paint),繪製頁面的像素信息 5. 瀏覽器主進程將默認的圖層和複合圖層交給 GPU 進程,GPU 進程再將各個圖層合成(composite),最後顯示出頁面
關鍵渲染路徑是瀏覽器將 HTML、CSS、JavaScript 轉換爲在屏幕上呈現的像素內容所經歷的一系列步驟。也就是咱們剛剛提到的的的瀏覽器渲染流程。java
爲儘快完成首次渲染,咱們須要最大限度減少如下三種可變因素:node
* 關鍵資源的數量: 可能阻止網頁首次渲染的資源。 * 關鍵路徑長度: 獲取全部關鍵資源所需的往返次數或總時間。 * 關鍵字節: 實現網頁首次渲染所需的總字節數,等同於全部關鍵資源傳送文件大小的總和。
* 刪除沒必要要的代碼和註釋包括空格,儘可能作到最小化文件。 * 能夠利用 GZIP 壓縮文件。 * 結合 HTTP 緩存文件。
首先,DOM 和 CSSOM 一般是並行構建的,因此 CSS 加載不會阻塞 DOM 的解析。webpack
然而,因爲 Render Tree 是依賴於 DOM Tree 和 CSSOM Tree 的,
因此他必須等待到 CSSOM Tree 構建完成,也就是 CSS 資源加載完成(或者 CSS 資源加載失敗)後,才能開始渲染。所以,CSS 加載會阻塞 Dom 的渲染。git
因而可知,對於 CSSOM 縮小、壓縮以及緩存一樣重要,咱們能夠從這方面考慮去優化。github
* 減小關鍵 CSS 元素數量 * 當咱們聲明樣式表時,請密切關注媒體查詢的類型,它們極大地影響了 CRP 的性能 。
當瀏覽器遇到 script 標記時,會阻止解析器繼續操做,直到 CSSOM 構建完畢,JavaScript 纔會運行並繼續完成 DOM 構建過程。
* async: 當咱們在 script 標記添加 async 屬性之後,瀏覽器遇到這個 script 標記時會繼續解析 DOM,同時腳本也不會被 CSSOM 阻止,即不會阻止 CRP。 * defer: 與 async 的區別在於,腳本須要等到文檔解析後( DOMContentLoaded 事件前)執行,而 async 容許腳本在文檔解析時位於後臺運行(二者下載的過程不會阻塞 DOM,但執行會)。 * 當咱們的腳本不會修改 DOM 或 CSSOM 時,推薦使用 async 。 * 預加載 —— preload & prefetch 。 * DNS 預解析 —— dns-prefetch 。
* 分析並用 **關鍵資源數 關鍵字節數 關鍵路徑長度** 來描述咱們的 CRP 。 * 最小化關鍵資源數: 消除它們(內聯)、推遲它們的下載(defer)或者使它們異步解析(async)等等 。 * 優化關鍵字節數(縮小、壓縮)來減小下載時間 。 * 優化加載剩餘關鍵資源的順序: 讓關鍵資源(CSS)儘早下載以減小 CRP 長度 。
補充閱讀: 前端性能優化之關鍵路徑渲染優化
迴流必將引發重繪,重繪不必定會引發迴流。
當頁面中元素樣式的改變並不影響它在文檔流中的位置時(例如:color、background-color、visibility 等),瀏覽器會將新樣式賦予給元素並從新繪製它,這個過程稱爲重繪。
當 Render Tree 中部分或所有元素的尺寸、結構、或某些屬性發生改變時,瀏覽器從新渲染部分或所有文檔的過程稱爲迴流。
會致使迴流的操做:
* 頁面首次渲染 * 瀏覽器窗口大小發生改變 * 元素尺寸或位置發生改變元素內容變化(文字數量或圖片大小等等) * 元素字體大小變化 * 添加或者刪除可見的 DOM 元素 * 激活 CSS 僞類(例如:hover) * 查詢某些屬性或調用某些方法 * 一些經常使用且會致使迴流的屬性和方法
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、 getBoundingClientRect()、scrollTo()
迴流比重繪的代價要更高。
有時即便僅僅迴流一個單一的元素,它的父元素以及任何跟隨它的元素也會產生迴流。現代瀏覽器會對頻繁的迴流或重繪操做進行優化:瀏覽器會維護一個隊列,把全部引發迴流和重繪的操做放入隊列中,若是隊列中的任務數量或者時間間隔達到一個閾值的,瀏覽器就會將隊列清空,進行一次批處理,這樣能夠把屢次迴流和重繪變成一次。
當你訪問如下屬性或方法時,瀏覽器會馬上清空隊列:
clientWidth、clientHeight、clientTop、clientLeft offsetWidth、offsetHeight、offsetTop、offsetLeft scrollWidth、scrollHeight、scrollTop、scrollLeft width、height getComputedStyle() getBoundingClientRect()
由於隊列中可能會有影響到這些屬性或方法返回值的操做,即便你但願獲取的信息與隊列中操做引起的改變無關,瀏覽器也會強行清空隊列,確保你拿到的值是最精確的。
CSS
Javascript
// 優化前 const el = document.getElementById('test'); el.style.borderLeft = '1px'; el.style.borderRight = '2px'; el.style.padding = '5px'; // 優化後,一次性修改樣式,這樣能夠將三次重排減小到一次重排 const el = document.getElementById('test'); el.style.cssText += '; border-left: 1px ;border-right: 2px; padding: 5px;'
圖片懶加載在一些圖片密集型的網站中運用比較多,經過圖片懶加載可讓一些不可視的圖片不去加載,避免一次性加載過多的圖片致使請求阻塞(瀏覽器通常對同一域名下的併發請求的鏈接數有限制),這樣就能夠提升網站的加載速度,提升用戶體驗。
將頁面中的img標籤src指向一張小圖片或者src爲空,而後定義data-src(這個屬性能夠自定義命名,我才用data-src)屬性指向真實的圖片。src指向一張默認的圖片,不然當src爲空時也會向服務器發送一次請求。能夠指向loading的地址。注意,圖片要指定寬高。
<img src="default.jpg" data-src="666.jpg" />
當載入頁面時,先把可視區域內的img標籤的data-src屬性值負給src,而後監聽滾動事件,把用戶即將看到的圖片加載。這樣便實現了懶加載。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <style> img { display: block; margin-bottom: 50px; width: 400px; height: 400px; } </style> </head> <body> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <script> let num = document.getElementsByTagName('img').length; let img = document.getElementsByTagName("img"); let n = 0; //存儲圖片加載到的位置,避免每次都從第一張圖片開始遍歷 lazyload(); //頁面載入完畢加載但是區域內的圖片 window.onscroll = lazyload; function lazyload() { //監聽頁面滾動事件 let seeHeight = document.documentElement.clientHeight; //可見區域高度 let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; //滾動條距離頂部高度 for (let i = n; i < num; i++) { if (img[i].offsetTop < seeHeight + scrollTop) { if (img[i].getAttribute("src") == "Go.png") { img[i].src = img[i].getAttribute("data-src"); } n = i + 1; } } } </script> </body> </html>
事件委託其實就是利用JS事件冒泡機制把本來須要綁定在子元素的響應事件(click、keydown……)委託給父元素,讓父元素擔當事件監聽的職務。事件代理的原理是DOM元素的事件冒泡。
優勢:
1. 大量減小內存佔用,減小事件註冊。 2. 新增元素實現動態綁定事件
例若有一個列表須要綁定點擊事件,每個列表項的點擊都須要返回不一樣的結果。
傳統寫法:
<ul id="color-list"> <li>red</li> <li>yellow</li> <li>blue</li> <li>green</li> <li>black</li> <li>white</li> </ul> <script> (function () { var color_list = document.querySelectorAll('li') console.log("color_list", color_list) for (let item of color_list) { item.onclick = showColor; } function showColor(e) { alert(e.target.innerHTML) console.log("showColor -> e.target", e.target.innerHTML) } })(); </script>
傳統方法會利用for循環遍歷列表爲每個列表元素綁定點擊事件,當列表中元素數量很是龐大時,須要綁定大量的點擊事件,這種方式就會產生性能問題。這種狀況下利用事件委託就能很好的解決這個問題。
改用事件委託:
<ul id="color-list"> <li>red</li> <li>yellow</li> <li>blue</li> <li>green</li> <li>black</li> <li>white</li> </ul> <script> (function () { var color_list = document.getElementByid('color-list'); color_list.addEventListener('click', showColor, true); function showColor(e) { var x = e.target; if (x.nodeName.toLowerCase() === 'li') { alert(x.innerHTML); } } })(); </script>
輸入搜索時,能夠用防抖debounce等優化方式,減小http請求;
這裏以滾動條事件舉例:防抖函數 onscroll 結束時觸發一次,延遲執行
function debounce(func, wait) { let timeout; return function() { let context = this; // 指向全局 let args = arguments; if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { func.apply(context, args); // context.func(args) }, wait); }; } // 使用 window.onscroll = debounce(function() { console.log('debounce'); }, 1000);
節流函數:只容許一個函數在N秒內執行一次。滾動條調用接口時,能夠用節流throttle等優化方式,減小http請求;
下面仍是一個簡單的滾動條事件節流函數:節流函數 onscroll 時,每隔一段時間觸發一次,像水滴同樣
function throttle(fn, delay) { let prevTime = Date.now(); return function() { let curTime = Date.now(); if (curTime - prevTime > delay) { fn.apply(this, arguments); prevTime = curTime; } }; } // 使用 var throtteScroll = throttle(function() { console.log('throtte'); }, 1000); window.onscroll = throtteScroll;
Vue 應用的性能問題能夠分爲兩個部分,第一部分是運行時性能問題,第二部分是加載性能問題。
和其餘 web 應用同樣,定位 Vue 應用性能問題最好的工具是 Chrome Devtool,經過 Performance 工具能夠用來錄製一段時間的 CPU 佔用、內存佔用、FPS 等運行時性能問題,經過 Network 工具能夠用來分析加載性能問題。
更多 Chrome Devtool 使用方式請參考 使用 Chrome Devtool 定位性能問題 的指南
運行時性能主要關注 Vue 應用初始化以後對 CPU、內存、本地存儲等資源的佔用,以及對用戶交互的及時響應。
開發環境下,Vue 會提供不少警告來幫你對付常見的錯誤與陷阱。而在生產環境下,這些警告語句沒有用,反而會增長應用的體積。有些警告檢查還有一些小的運行時開銷。
當使用 webpack 或 Browserify 相似的構建工具時,Vue 源碼會根據 process.env.NODE_ENV 決定是否啓用生產環境模式,默認狀況爲開發環境模式。在 webpack 與 Browserify 中都有方法來覆蓋此變量,以啓用 Vue 的生產環境模式,同時在構建過程當中警告語句也會被壓縮工具去除。
詳細的作法請參閱 生產環境部署
當使用 DOM 內模板或 JavaScript 內的字符串模板時,模板會在運行時被編譯爲渲染函數。一般狀況下這個過程已經足夠快了,但對性能敏感的應用仍是最好避免這種用法。
預編譯模板最簡單的方式就是使用單文件組件——相關的構建設置會自動把預編譯處理好,因此構建好的代碼已經包含了編譯出來的渲染函數而不是原始的模板字符串。
詳細的作法請參閱 預編譯模板
當使用單文件組件時,組件內的 CSS 會以 <style> 標籤的方式經過 JavaScript 動態注入。這有一些小小的運行時開銷,將全部組件的 CSS 提取到同一個文件能夠避免這個問題,也會讓 CSS 更好地進行壓縮和緩存。
查閱這個構建工具各自的文檔來了解更多:
Object.freeze() 能夠凍結一個對象,凍結以後不能向這個對象添加新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該對象已有屬性的可枚舉性、可配置性、可寫性。該方法返回被凍結的對象。
當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter,這些 getter/setter 對用戶來講是不可見的,可是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化。
但 Vue 在遇到像 Object.freeze() 這樣被設置爲不可配置以後的對象屬性時,不會爲對象加上 setter getter 等數據劫持的方法。 參考 Vue 源碼
Object.freeze()### 應用場景
因爲 Object.freeze() 會把對象凍結,因此比較適合展現類的場景,若是你的數據屬性須要改變,能夠從新替換成一個新的 Object.freeze()的對象。
不少時候,咱們會發現接口返回的信息是以下的深層嵌套的樹形結構:
{ "id": "123", "author": { "id": "1", "name": "Paul" }, "title": "My awesome blog post", "comments": [ { "id": "324", "commenter": { "id": "2", "name": "Nicole" } } ] }
假如直接把這樣的結構存儲在 store 中,若是想修改某個 commenter 的信息,咱們須要一層層去遍歷找到這個用戶的信息,同時有可能這個用戶的信息出現了屢次,還須要把其餘地方的用戶信息也進行修改,每次遍歷的過程會帶來額外的性能開銷。
假設咱們把用戶信息在 store 內統一存放成 users[id]這樣的結構,修改和讀取用戶信息的成本就變得很是低。
你能夠手動去把接口裏的信息經過相似數據的表同樣像這樣存起來,也能夠藉助一些工具,這裏就須要提到一個概念叫作 JSON數據規範化(normalize), Normalizr 是一個開源的工具,能夠將上面的深層嵌套的 JSON 對象經過定義好的 schema 轉變成使用 id 做爲字典的實體表示的對象。
當你有讓 Vue App 離線可用,或者有接口出錯時候進行災備的需求的時候,你可能會選擇把 Store 數據進行持久化,這個時候須要注意如下幾個方面:
Vue 社區中比較流行的 vuex-persistedstate,利用了 store 的 subscribe 機制,來訂閱 Store 數據的 mutation,若是發生了變化,就會寫入 storage 中,默認用的是 localstorage 做爲持久化存儲。
也就是說默認狀況下每次 commit 都會向 localstorage 寫入數據,localstorage 寫入是同步的,並且存在不小的性能開銷,若是你想打造 60fps 的應用,就必須避免頻繁寫入持久化數據。
咱們應該儘可能減小直接寫入 Storage 的頻率:
* 屢次寫入操做合併爲一次,好比採用函數節流或者將數據先緩存在內存中,最後在一併寫入 * 只有在必要的時候才寫入,好比只有關心的模塊的數據發生變化的時候才寫入
因爲持久化緩存的容量有限,好比 localstorage 的緩存在某些瀏覽器只有 5M,咱們不能無限制的將全部數據都存起來,這樣很容易達到容量限制,同時數據過大時,讀取和寫入操做會增長一些性能開銷,同時內存也會上漲。
尤爲是將 API 數據進行 normalize 數據扁平化後以後,會將一份數據散落在不一樣的實體上,下次請求到新的數據也會散落在其餘不一樣的實體上,這樣會帶來持續的存儲增加。
所以,當設計了一套持久化的數據緩存策略的時候,同時應該設計舊數據的緩存清除策略,例如請求到新數據的時候將舊的實體逐個進行清除。
若是你的應用存在很是長或者無限滾動的列表,那麼採用 窗口化 的技術來優化性能,只須要渲染少部分區域的內容,減小從新渲染組件和建立 dom 節點的時間。
vue-virtual-scroll-list 和 vue-virtual-scroller 都是解決這類問題的開源項目。你也能夠參考 Google 工程師的文章 Complexities of an Infinite Scroller 來嘗試本身實現一個虛擬的滾動列表來優化性能,主要使用到的技術是 DOM 回收、墓碑元素和滾動錨定。
Google 工程師繪製的無限列表設計
上面提到的無限列表的場景,比較適合列表內元素很是類似的狀況,不過有時候,你的 Vue 應用的超長列表內的內容每每不盡相同,例如在一個複雜的應用的主界面中,整個主界面由很是多不一樣的模塊組成,而用戶看到的每每只有首屏一兩個模塊。在初始渲染的時候不可見區域的模塊也會執行和渲染,帶來一些額外的性能開銷。
使用組件懶加載在不可見時只須要渲染一個骨架屏,不須要真正渲染組件
你能夠對組件直接進行懶加載,對於不可見區域的組件內容,直接不進行加載和初始化,避免初始化渲染運行時的開銷。具體能夠參考咱們以前的專欄文章 性能優化之組件懶加載: Vue Lazy Component 介紹 ,瞭解如何作到組件粒度的懶加載。
在一個單頁應用中,每每只有一個 html 文件,而後根據訪問的 url 來匹配對應的路由腳本,動態地渲染頁面內容。單頁應用比較大的問題是首屏可見時間過長。
單頁面應用顯示一個頁面會發送屢次請求,第一次拿到 html 資源,而後經過請求再去拿數據,再將數據渲染到頁面上。並且因爲如今微服務架構的存在,還有可能發出屢次數據請求才能將網頁渲染出來,每次數據請求都會產生 RTT(往返時延),會致使加載頁面的時間拖的很長。
服務端渲染、預渲染和客戶端渲染的對比
這種狀況下能夠採用服務端渲染(SSR)和預渲染(Prerender)來提高加載性能,這兩種方案,用戶讀取到的直接就是網頁內容,因爲少了節省了不少 RTT(往返時延),同時,還能夠對一些資源內聯在頁面,能夠進一步提高加載的性能。
能夠參考專欄文章 優化向:單頁應用多路由預渲染指南 瞭解如何利用預渲染進行優化。
服務端渲染(SSR)能夠考慮使用 Nuxt 或者按照 Vue 官方提供的 Vue SSR 指南 來一步步搭建。
在上面提到的超長應用內容的場景中,經過組件懶加載方案能夠優化初始渲染的運行性能,其實,這對於優化應用的加載性能也頗有幫助。
組件粒度的懶加載結合異步組件和 webpack 代碼分片,能夠保證按需加載組件,以及組件依賴的資源、接口請求等,比起一般單純的對圖片進行懶加載,更進一步的作到了按需加載資源。
使用組件懶加載以前的請求瀑布圖
使用組件懶加載以後的請求瀑布圖
使用組件懶加載方案對於超長內容的應用初始化渲染頗有幫助,能夠減小大量必要的資源請求,縮短渲染關鍵路徑,具體作法請參考咱們以前的專欄文章 性能優化之組件懶加載: Vue Lazy Component 介紹 。
上面部分總結了 Vue 應用運行時以及加載時的一些性能優化措施,下面作一個回顧和歸納:
Vue 應用運行時性能優化措施
Vue 應用加載性能優化措施
文章總結的這些性能優化手段固然不能覆蓋全部的 Vue 應用性能問題,咱們也會不斷總結和補充其餘問題及優化措施,但願文章中提到這些實踐經驗能給你的 Vue 應用性能優化工做帶來小小的幫助。
參考:
從 8 道面試題看瀏覽器渲染過程與性能優化
Vue 應用性能優化指南
前端性能優化的經常使用手段
前端性能優化之關鍵路徑渲染優化
網頁頁面性能優化總結
推薦閱讀:
【專題:JavaScript進階之路】
JavaScript中各類源碼實現(前端面試筆試必備)
深刻理解 ES6 Promise
JavaScript之函數柯理化
ES6 尾調用和尾遞歸
Git經常使用命令小結
淺談 MVC 和 MVVM 模型
我是Cloudy,現居上海,年輕的前端攻城獅一枚,愛專研,愛技術,愛分享。
我的筆記,整理不易,感謝關注
、閱讀
、點贊
和收藏
。
文章有任何問題歡迎你們指出,也歡迎你們一塊兒交流各類前端問題!