前端性能優化
- 減少HTTP請求數量
- CSS Sprites
- 內聯圖片(圖片base64)
- 最大化合並JS、CSS模塊
- 利用瀏覽器緩存
- 減小HTTP請求大小
- 壓縮HTTP響應包(Accept-Encoding: gzip, deflate)
- 壓縮HTML、CSS、JS模塊
- DOM方面
- 離線操作DOM
- 使用innerHTML進行大量的DHTML操作
- 使用事件代理
- 緩存布局信息
- 移除頁面上不存在的事件處理程序
- JavaScript語言本身的優化
- 使用局部變量代替全部變量,減少作用域鏈遍歷標識符的時間
- 減少對象成員及數組項的查找次數
- 避免使用with語句和eval函數
- ajax優化
- get或者post請求
- multipart XHR
- ajax緩存
- 其他方面的性能優化
- 使用CDN加載靜態資源
- CSS樣式放在頭部
- JS腳本放在底部
- 避免使用CSS表達式
- 外聯JS、CSS
- 減少DNS查找
- 避免URL重定向
轉載請注明出處: 前端性能優化
減少HTTP請求數量
CSS Sprites
將多個圖片合並成一張圖,只像圖片發送一次請求的技術。此時可以通過background-position
根據位置定位到不同的圖片。雖然合並之后的一張圖片包含附加的空白區域,會讓人覺得比單個圖片合並起來的圖片要大。實際上,合並后的圖片會比分離的圖片的總和要小,因為一來將多次請求合並成了一次,二來降低了圖片自身的開銷(顏色表,格式信息等等)。
舉個例子,如果有需要請求四個25k的圖片,那么直接請求100k的圖片會比發送四次請求要快一些。因為多次http請求會產生性能開銷和圖片自身的開銷。
內聯圖片
通過使用data: URL模式可以在Web頁面包含圖片但無需任何額外的HTTP請求。data: URL中的URL是經過base64編碼的。格式如下
<img src="data:image/gif;base64....." alt="home">
由於使用內聯圖片(圖片base64)是內聯在HTML中的,因此在跨越頁面時不會被緩存。一般情況下,不要將網站的Logo做圖片base64的處理,因為編碼過的Logo會導致頁面變大。可將圖片作為背景,放在CSS樣式表中,此時CSS可被瀏覽器緩存
.home {
background-image: url(data:image/gif;base64.....)
}
最大化JS、CSS的合並
考慮到HTTP請求會帶來額外的性能開銷,因此下載單個100kb的文件比下載4個25kb的文件更快。最大化合並JS、CSS將會改善性能。
利用瀏覽器緩存
減少呈現頁面時所必需的HTTP請求的數量是加速用戶體驗的最佳方式。可以通過最大化瀏覽器緩存組件的能力來實現。
什么是緩存
如果組件(HTML、CSS、JavsScript、圖片資源等)被緩存到瀏覽器中,在下次再次加載的時候有可能從組件中獲取緩存,而不是向服務器發送HTTP請求。減少HTTP請求有利於前端性能優化。
瀏覽器如何緩存
瀏覽器在下載組件(HTML、CSS、JavsScript、圖片資源等),會將他們緩存到瀏覽器中。如果某個組件確實更新了,但是仍然在緩存中。這時候可以給組件添加版本號的方式(md5)避免讀取緩存。
瀏覽器再次下載組件時,如何確認是緩存的組件
1.Expires頭
可以通過服務端配置,將某個組件的過期時間設置的長一些。比如,公司Logo不會經常變化等。瀏覽器在下載組件時,會將其緩存。在后續頁面的查看中,如果在指定時間內,表明組件是未過期的,則可以直接讀取緩存,而不用走HTTP請求。如果在指定時間外,則表明組件是過期的,此時並不會馬上發起一個HTTP請求,而是發起一個條件GET請求。
2.條件GET請求
如果緩存的組件過期了(或者用戶reload,refresh了頁面),瀏覽器在重用它之前必須先檢查它是否仍然有效。這稱為一個條件GET請求。這個請求是瀏覽器必須發起的。如果響應頭部的Last-Modified(最后修改時間,服務器傳回的值)與請求頭部的If-Modified-Since(最新修改時間)得值匹配,則會返回304響應(Not-Modified),即直接從瀏覽器中讀取緩存,而不是走HTTP請求。
3.Etag(實體標簽)
Etag其實和條件GET請求很像,也是通過檢測瀏覽器緩存中的組件與原始服務器上的組件是否匹配。如果響應頭部的Etag與請求頭部的If-None-Match的值互相匹配,則會返回304響應。
Etag存在的一些問題:
- 如果只有一台服務器,使用Etag沒有什么問題。如果有多台服務器,從不同服務器下載相同的組件返回的Etag會不同,即使內容相同,也不會從緩存中讀取,而是發起HTTP請求。
- Etag降低了代理緩存的效率。
- If-None-Match比If-Modified-Since擁有更高的優先級。即使條件GET請求的響應頭部和請求頭部的兩個值相同,在擁有多台服務器的情況下,不是從緩存中讀取,而是仍然會發起HTTP請求。
有兩種方式可以解決這個問題
- 在服務端配置Etag。
- 在服務端移除Etag。移除Etag可以減少響應和后續HTTP請求頭的大小。Last-Modified可以提供完全等價的信息
減少HTTP請求大小
1.組件(HTML, CSS, JavaScript)壓縮處理
2.配置請求頭部信息:Accept-encoding: gzip, deflate。此時服務器返回的響應頭部中會包含Content-encoding: gzip的信息,表明http響應包被壓縮。
DOM方面
離線DOM操作
如果需要給頁面上某個元素進行某種DOM操作時(如增加某個子節點或者增加某段文字或者刪除某個節點),如果直接對在頁面上進行更新,此時瀏覽器需要重新計算頁面上所有DOM節點的尺寸,進行重排和重繪。現場進行的DOM更新越多,所花費的時間就越長。重排是指某個DOM節點發生位置變化時(刪除、移動、CSS盒模型等),重新繪制渲染樹🌲的過程。重繪是指將發生位置變化的DOM節點重新繪制到頁面上的過程。
var list = document.getElementById("myList"),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
以上元素進行了20次現場更新,有10次是將li插入到list元素中,另外10次文本節點。這里就產生了20次DOM的重排和重繪。此時可以采用以下方法, 來減少DOM元素的重拍和重繪。
一是采用文檔碎片(),一是將li元素最后才插入到頁面上
一:使用文檔碎片(推薦)
var list = document.getElementById("myList"),
item,
i,
frag = document.createDocumentFragment(); // 文檔碎片
for (i=0; i < 10; i++) {
item = document.createElement("li");
frag.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
document.body.appendChild(frag)
二:循環結束時插入li
var list = document.getElementById("myList"),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(item);
采用innerHTML方法
有兩種在頁面上創建 DOM 節點的方法:使用諸如 createElement()和 appendChild()之類的DOM 方法,以及使用innerHTML。對於小的DOM更改而言,兩種方法效率都差不多。然而,對於大的 DOM 更改,使用 innerHTML 要比使用標准 DOM 方法創建同樣的 DOM 結構快得多。當把innerHTML設置為某個值時,后台會創建一個HTML解析器,然后使用內部的DOM 調用來創建 DOM 結構,而非基於JavaScript的DOM調用。由於內部方法是編譯好的而非解釋執行的,所以執行快得多。
var ul = document.querySelector('ul')
var html = ''
for (var i = 0; i < 10; i++) {
html += '<li>'+ i +'</li>'
// 避免在for循環中使用innerHTML, 因為在循環中使用innerHTML會導致現場更新!
}
ul.innerHTML = html // 循環結束時插入到ul元素中
這段代碼構建了一個 HTML 字符串,然后將其指定到 list.innerHTML,便創建了需要的DOM結構。雖然字符串連接上總是有點性能損失,但這種方式還是要比進行多個DOM操作更快。
緩存布局信息
當在實際應用中需要獲取頁面上某個DOM節點的布局信息時,如offset dimension, client dimension或者是樣式等,瀏覽器為了返回最新值,會刷新整個DOM樹去獲取。最好的做法是緩存布局信息,減少布局信息的獲取次數。獲取之后將其緩存到局部變量中,然后再操作此局部變量。
如,需要將某個DOM節點沿對角線移動,一次移動一個像素,從100*100 移動到500 * 500。
如果這樣做,對於性能優化來說是低效的。
div.style.left = 1 + div.clientLeft + 'px'
div.style.top = 1 + div.clientTop + 'px'
if (div.style.clientLeft >= 500 && div.style.clientTop >= 500) {
// 停止累加..
}
下面使用局部變量緩存布局信息,對於性能優化來說是高效的。
let left = div.clientLeft, right = div.clientTop
div.style.left = 1 + left + 'px'
div.style.top = 1 + right+ 'px'
if (div.style.clientLeft >= 500 && div.style.clientTop >= 500) {
// 停止累加..
}
事件代理
在javascript中,在頁面渲染時添加到頁面上的事件處理程序數量直接關系到頁面的整體運行性能。最直接的影響是頁面的事件處理程序越多,訪問DOM節點的次數也就越多。另外函數是對象,會占用內存。內存中的對象越多,性能就越差。
事件代理就是解決'過多的事件處理程序'的。事件代理基於事件冒泡機制。因此,可以將同一事件類型的事件都綁定到document對象上,根據事件對象的target屬性下的id, class 或者name屬性,判斷需要給哪個DOM節點綁定事件處理程序。這種事件代理機制在頁面渲染時將訪問多次DOM節點減少到了一次,因為此時我們只需訪問document對象。如下實現
document.addEventListener('click', function (e) {
switch (e.target.id) {
case 'new':
console.log('new')
break
case 'name':
console.log('name')
break
case 'sex':
console.log('sex')
break
}
}, false)
使用事件代理有以下優點:
- 可以在頁面生名周期的任何時間點上添加添加事件處理程序(無需等待DOMContentLoaded和Load事件)。換句話說,只要某個需要添加事件處理程序的元素存在頁面上,就可以綁定相應的事件。
- DOM節點訪問次數減少。
- 事件處理程序時函數,而函數是對象。對象會占用內存。事件處理程序減少了,所占用的內存空間就少了,就能夠提升整體性能。
移除事件處理程序
假設有這樣一個需求:頁面上有一個按鈕,在點擊時需要替換成某個文本。如果直接替換該按鈕,由於該按鈕的事件處理程序已經存在內存中了,此時移除按鈕並沒有將事件處理程序一同移除,頁面仍然持有對該按鈕事件處理程序的引用。一旦這種情況出現多次,那么原來添加到元素中的事件處理程序會占用內存。在事件代理中也談過,函數是對象,內存中的對象越多,性能有越差。除了文本替換外,還可能出現在移除(removeChild)、替換(replaceChild)帶有事件處理程序的DOM節點。
而正確的做法是,在移除該按鈕的同時,移除事件處理程序。
<div class="content">
<button class='btn'>點擊</button>
</div>
var btn = document.querySelector('.btn')
btn.addEventListener('click', function func(e) {
btn.removeEventListener('click', func, false) // 在替換前,移除該按鈕的事件處理程序
document.querySelector('.content').innerHTML = '替換button按鈕拉!'
}, false)
JavaScript的優化
使用局部變量代替全局變量,減少在作用域鏈上搜索標識符的時間
在JavaScript中,作用域分為函數作用域和詞法作用域。當我們執行了某個函數時,會創建一個執行環境。如果在執行環境中想搜索某個變量,會經歷以下行為:
首先從當前詞法作用域開始搜索,如果找到了這個變量,那么就停止搜索,返回該變量;如果找不到,那么就會搜索外層的詞法作用域,一直向上冒泡;如果仍然沒有在全局作用域下仍然沒有搜索到該變量,瀏覽器就會報RefferceError類型的錯誤,此錯誤表示與作用域相關。最后,此函數的執行環境被銷毀。
從性能方面思考,如果將某個變量放在全局作用域下,那么讀寫到該變量的時間會比局部變量多很多。變量在作用域中的位置越深,訪問所需時間就越長。由於全局變量總是(document, window對象)處在作用域鏈的最末端,因此訪問速度是最慢的。
舉個例子吧。比如我們操作DOM元素時,必不可免的會使用到document對象。這個對象是window對象下的一個屬性,也算是一個全局變量吧。因此,當我們操作DOM時,可以將其緩存,作為局部變量存在,那么就避免了作用域鏈搜索全局變量的過程。
let func = () => {
let doc = document // document作為局部變量存在
let body = doc.body // body作為局部變量存在
let p = doc.createElement('p')
let text = doc.createTextNode('document和body作為局部變量存在')
body.appendChld(p)
}
減少對象成員數組項的查找次數
這點主要體現在循環體上。以for循環為例,緩存數組長度,而不是在每次循環中獲取。
假設有有一個arr數組,長度為50000
// 低效的, 每次都要獲取數組長度
for (var i = 0; i < arr.length; i++) {
// do something...
}
// for循環性能優化:緩存數組長度
for ( var i = 0, len = arr.length; i < len; i++) {
// do something
}
Ajax方面的優化
get或者post請求
這里可以扯一下get和post請求的區別。
對於get請求來說,主要用於獲取(查詢)數據。get請求的參數需要以query string的方式添加在URL后面的。當我們需要從服務器獲取或者查詢某數據時,都應該使用get請求。優點在於gei請求比post請求要快,同時get請求可以被瀏覽器緩存。缺點在於get請求的參數大於2048個字符時,超過的字符會被截取,此時需要post請求。
對於post請求來說,主要用於保存(增加值、修改值、刪除值)數據。post請求的參數是作為請求的主體提交到服務器。優點在於沒有字節的限制。缺點是無法被瀏覽器緩存。
get和post請求有一個共同點:雖然在請求時,get請求將參數帶在url后面,post請求將參數作為請求的主體提交。但是請求參數都是以name1=value1&name2=value2
的方式發送到服務器的。
let data ['name1=value1', 'name2=value2']
let xhr = new window.XMLHttpRequest()
xhr.addEventListener('readystatechange', () => {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.responseText)
}
}
}, false)
let getPram = '?' + data.join('&')
let postPram = data.join('&')
// open方法:
xhr.open('get', 'url' + getPram, true)
// post方法, 作為請求的主體提交
// xhr.send(postPram)
所以,扯了那么多。要注意的是,get請求用於查詢(獲取)數據,post請求用於保存(增刪改)數據。
跨域JSONP
由於同源政策的限制,ajax只能在同域名、同協議、同端口的情況下才可以訪問。也就是說,跨域是不行的。但是可以使用JSONP的方式繞過同源政策。
JSONP實現的原理:動態創建script標簽。通過src屬性添加需要訪問的地址,將返回的數據作為參數封裝在回調函數中
let script = document.createElement('script')
script.src = 'url...'
script.id = 'script'
document.head.appendChild(script)
script.addEventListener('load', e => {
if (this.readyState === 'complete') {
let data = e
// do something...
}
}, false)
JSONP的優點:
- 跨域請求。
- 由於返回的參數是JavaScript代碼,而不是作為字符串需要進一步處理。所以速度快
JSONP的缺點:
- 只能以get請求發送。
- 無法為錯誤、失敗事件設置事件處理程序。
- 無法設請求頭。
multipart XHR
暫時未使用過,占位占位、等使用過了再更新:)
ajax緩存
先占位。目前正在開發一個小型類jQuery庫。主要目的有:熟悉面向對象編程思想,熟悉DOM操作。到時候開發完ajax模塊再回來填坑。
其他方面的性能優化
將樣式表放在頂部
CSS樣式表可以放在兩個地方,一是文檔頭部,一是文檔底部。位置的不同會帶來不同的體驗。
當樣式表放在文檔底部時,不同瀏覽器會出現不同的效果
IE瀏覽器在新窗口打開、刷新頁面時,瀏覽器會阻塞內容的逐步呈現,取而代之的是白屏一段時間,等到CSS樣式下載完畢之后再將內容和樣式渲染到頁面上;在點擊鏈接、書簽欄、reload時,瀏覽器會先將內容逐步呈現,等到CSS樣式加載完畢之后重新渲染DOM樹,此時會發生無樣式內容的閃爍問題
火狐瀏覽器不管以什么方式打開瀏覽器都會將內容逐步呈現,然后等到css樣式加載完畢之后再重新渲染DOM樹,發生無樣式內容的閃爍的問題。
當樣式表放在文檔頂部時,雖然瀏覽器需要先加載CSS樣式,速度可能比放在底部的慢些,但是由於可以使頁面內容逐步呈現,所以對用戶來時還是快的。因為有內容呈現了而不是白屏,發生無樣式內容的閃爍,用戶體驗也會友好些。畢竟,有內容比白屏要好很多吧...
將樣式放在文檔頂部有兩種方式。當使用link標簽將樣式放在head時,瀏覽器會使內容逐步呈現,但是會發生無樣式內容的閃爍問題;當使用@import規則,由於會發生模塊(圖片、樣式、腳本)下載時的無序性,可能會出現白屏的現象。另外,在style標簽下可以使用多個import規則,但是必須放置在其他規則之前。link和@import引入樣式也存在性能問題,推薦引入樣式時都使用link標簽。
文章中,簡單的說就是都是用link標簽或者都是用@import規則加載CSS樣式時會並行下載;而混用link標簽和@import規則導致樣式無法並行下載,而是逐個下載。由於@import規則會導致模塊下載的無序性問題,所以還是推薦全部使用link標簽引入css樣式
將腳本放在底部
將腳本放在文檔頂部會導致如下問題:
- 腳本會阻塞其后組件的並行下載和執行
- 腳本會阻塞其后頁面的逐步呈現
HTTP1.1規定,建議每個瀏覽器從服務器並行下載兩個組件。這也意味着,增加服務器的數量,並行下載的數量也會增加。如果有兩台服務器,那么並行下載組件的數量為4。
除了將腳本放在底部可以解決這個以上兩個問題,script標簽`的async和defer屬性也可以解決這兩個問題。
asnyc屬性(異步腳本)表示腳本可以立即下載,下載完成后自動執行,但不應妨礙頁面中的其他操作。比如下載其他模塊(圖片、樣式、腳本)。由於是異步的,所以腳本下載沒有先后順序,沒有順序的腳本就要確保每個腳本不會互相依賴。只對外部腳本文件有效。異步腳本一定會在頁面load事件前執行,但可能會在DOMContentLoaded事件觸發前后執行。由於async屬性可以異步加載腳本,所以可以放在頁面的任何位置。
defer屬性(延遲腳本)表示腳本可以立即下載,但是會延遲到文檔完全被解析和顯示之后再執行。在DOMContentLoaded事件之后,load事件之前執行。由於defer屬性可以延遲腳本的執行,因此可以放在頁面的任何位置。
在沒有asnyc屬性和defer屬性的script標簽時,由於js是單線程的原因,所以只能下載完第一個script才能下載第二個,才到第三個,第四個......
避免使用CSS表達式
這個應該很少人用吧...畢竟網上對css表達式介紹的少之又少...反正我是沒用過的
外聯javascript、css
外聯javascript、css文件相對於內聯有以下優點。外聯的方式可以通過script標簽或者link標簽引入,也可以通過動態方式創建script標簽和link標簽(動態腳本、動態樣式),此時通過動態方式創建的腳本和樣式不會阻塞頁面其他組件的下載和呈現。
通用函數
let loadScript = (url, cb) => {
let script = document.createElement('script')
支持readystatechange事件的瀏覽器有IE、Firefox4+和Opera,谷歌不支持該事件。存在兼容性問題。
if (script.readyState) {
script.addEventListener('readystatechange', function change () {
if (script.readyState === 'loaded' || script.readyState === 'complete') {
// 移除readystatechange,避免觸發兩次
script.removeEventListener('readystatechange', change, false)
cb()
}
}, false)
} else {
script.addEventListener('load', () => {
cb()
}, false)
}
script.src = url
document.body.appendChild(script)
}
// 依次解析和執行a.js、b.js、c.js。
loadScript('./a.js', () => {
alert('a done')
loadScript('./b.js', () => {
alert('b done')
loadScript('./c.js', () => {
alert('c done')
})
})
})
- 可以被瀏覽器緩存。
- 作為組件復用。
減少DNS查找
DNS的作用是將域名解析為IP地址。通常情況下,瀏覽器查找一個給定主機名的IP地址需要花費20-120ms。在DNS服務器查找完成之前,瀏覽器不能從服務器那里下載任何東西。減少DNS查找的方法如下。
- 減少服務器數量。減少服務器數量意味着並行下載組件的數量也會減少,但是此時會減少DNS查找的時間。應根據具體業務場景做取舍。
- 瀏覽器緩存DNS記錄。可以通過服務器配置DNS緩存的時間。
- 配置
Keep-alive
。由於客戶端服務器連接是持久的,因此無需DNS查找。
避免url重定向
先占位。