從輸入 URL 到頁面加載完成的過程:javascript
咱們從輸入 URL 到顯示頁面這個過程當中,涉及到網絡層面的,有三個主要過程:html
對於 DNS 解析和 TCP 鏈接兩個步驟,咱們前端能夠作的努力很是有限。相比之下,HTTP 鏈接這一層面的優化纔是咱們網絡優化的核心。前端
HTTP 優化有兩個大的方向:java
瀏覽器緩存機制有四個方面,它們按照獲取資源時請求的優先級依次排列以下:算法
MemoryCache,是指存在內存中的緩存。從優先級上來講,它是瀏覽器最早嘗試去命中的一種緩存。從效率上來講,它是響應速度最快的一種緩存。瀏覽器秉承的是「節約原則」,咱們發現,Base64 格式的圖片,幾乎永遠能夠被塞進 memory cache,這能夠視做瀏覽器爲節省渲染開銷的「自保行爲」;此外,體積不大的 JS、CSS 文件,也有較大地被寫入內存的概率——相比之下,較大的 JS、CSS 文件就沒有這個待遇了,內存資源是有限的,它們每每被直接甩進磁盤。後端
Service Worker 是一種獨立於主線程以外的 Javascript 線程。它脫離於瀏覽器窗體,所以沒法直接訪問 DOM。這樣獨立的個性使得 Service Worker 的「我的行爲」沒法干擾頁面的性能,這個「幕後工做者」能夠幫咱們實現離線緩存、消息推送和網絡代理等功能。咱們藉助 Service worker 實現的離線緩存就稱爲 Service Worker Cache。瀏覽器
它又分爲強緩存和協商緩存。優先級較高的是強緩存,在命中強緩存失敗的狀況下,纔會走協商緩存。緩存
對一條http get 報文的基本緩存處理過程包括7個步驟:服務器
圖片描述網絡
強緩存是利用 http 頭中的 Expires 和 Cache-Control 兩個字段來控制的。強緩存中,當請求再次發出時,瀏覽器會根據其中的 expires 和 cache-control 判斷目標資源是否「命中」強緩存,若命中則直接從緩存中獲取資源,不會再與服務端發生通訊。
是否足夠新鮮時期:
經過 Expires: XXXX XXX XXX GMT (絕對日期時間,http/1.0) 或者 Cache-Control:max-age=XXXX (相對日期時間,http/1.1)在文檔標明過時日期。
Cache-Control 相對於 expires 更加準確,它的優先級也更高。當 Cache-Control 與 expires 同時出現時,咱們以 Cache-Control 爲準。
public 與 private 是針對資源是否可以被代理服務緩存而存在的一組對立概念。若是咱們爲資源設置了 public,那麼它既能夠被瀏覽器緩存,也能夠被代理服務器緩存;若是咱們設置了 private,則該資源只能被瀏覽器緩存。private 爲默認值。
no-store與no-cache,no-cache 繞開了瀏覽器:咱們爲資源設置了 no-cache 後,每一次發起請求都不會再去詢問瀏覽器的緩存狀況,而是直接向服務端去確認該資源是否過時(即走咱們下文即將講解的協商緩存的路線)。no-store 比較絕情,顧名思義就是不使用任何緩存策略。在 no-cache 的基礎上,它連服務端的緩存確認也繞開了,只容許你直接向服務端發送請求、並下載完整的響應。
協商緩存依賴於服務端與瀏覽器之間的通訊。協商緩存機制下,瀏覽器須要向服務器去詢問緩存的相關信息,進而判斷是從新發起請求、下載完整的響應,仍是從本地獲取緩存的資源。若是服務端提示緩存資源未改動(Not Modified),資源會被重定向到瀏覽器緩存,這種狀況下網絡請求對應的狀態碼是 304。
協商緩存的實現:從 Last-Modified 到 Etag,詳細本身百度,這裏再也不詳細展開。
圖片描述
當咱們的資源內容不可複用時,直接爲 Cache-Control 設置 no-store,拒絕一切形式的緩存;不然考慮是否每次都須要向服務器進行緩存有效確認,若是須要,那麼設 Cache-Control 的值爲 no-cache;不然考慮該資源是否能夠被代理服務器緩存,根據其結果決定是設置爲 private 仍是 public;而後考慮該資源的過時時間,設置對應的 max-age 和 s-maxage 值;最後,配置協商緩存須要用到的 Etag、Last-Modified 等參數。
Push Cache 是指 HTTP2 在 server push 階段存在的緩存。
CDN 的核心點有兩個,一個是緩存,一個是回源。
「緩存」就是說咱們把資源 copy 一份到 CDN 服務器上這個過程,「回源」就是說 CDN 發現本身沒有這個資源(通常是緩存的數據過時了),轉頭向根服務器(或者它的上層服務器)去要這個資源的過程。
CDN 每每被用來存放靜態資源。所謂「靜態資源」,就是像 JS、CSS、圖片等不須要業務服務器進行計算即得的資源。而「動態資源」,顧名思義是須要後端實時動態生成的資源,較爲常見的就是 JSP、ASP 或者依賴服務端渲染獲得的 HTML 頁面。
那「非純靜態資源」呢?它是指須要服務器在頁面以外做額外計算的 HTML 頁面。具體來講,當我打開某一網站以前,該網站須要經過權限認證等一系列手段確認個人身份、進而決定是否要把 HTML 頁面呈現給我。這種狀況下 HTML 確實是靜態的,但它和業務服務器的操做耦合,咱們把它丟到CDN 上顯然是不合適的。
另外,CDN的域名必須和主業務服務器的域名不同,要不,同一個域名下面的Cookie各處跑,浪費了性能流量的開銷,CDN域名放在不一樣的域名下,能夠完美地避免了沒必要要的 Cookie 的出現!
在計算機中,像素用二進制數來表示。不一樣的圖片格式中像素與二進制位數之間的對應關係是不一樣的。一個像素對應的二進制位數越多,它能夠表示的顏色種類就越多,成像效果也就越細膩,文件體積相應也會越大。
一個二進制位表示兩種顏色(0|1 對應黑|白),若是一種圖片格式對應的二進制位數有 n 個,那麼它就能夠呈現 2^n 種顏色。
對於一張 100 100 像素的圖片來講,圖像上有 10000 個像素點,若是每一個像素的值是 RGBA 存儲的話,那麼也就是說每一個像素有 4 個通道,每一個通道 1 個字節(8 位 = 1個字節),因此該圖片大小大概爲 39KB(10000 1 * 4 / 1024)。
可是在實際項目中,一張圖片可能並不須要使用那麼多顏色去顯示,咱們能夠經過減小每一個像素的調色板來相應縮小圖片的大小。
瞭解瞭如何計算圖片大小的知識,那麼對於如何優化圖片,想必你們已經有 2 個思路了:
JPEG/JPG 特色:有損壓縮、體積小、加載快、不支持透明,JPG 最大的特色是有損壓縮。這種高效的壓縮算法使它成爲了一種很是輕巧的圖片格式。另外一方面,即便被稱爲「有損」壓縮,JPG的壓縮方式仍然是一種高質量的壓縮方式:當咱們把圖片體積壓縮至原有體積的 50% 如下時,JPG 仍然能夠保持住 60% 的品質。但當它處理矢量圖形和 Logo 等線條感較強、顏色對比強烈的圖像時,人爲壓縮致使的圖片模糊會至關明顯。
PNG 特色:無損壓縮、質量高、體積大、支持透明,PNG(可移植網絡圖形格式)是一種無損壓縮的高保真的圖片格式。8 和 24,這裏都是二進制數的位數。按照咱們前置知識裏提到的對應關係,8 位的 PNG 最多支持 256 種顏色,而 24 位的能夠呈現約 1600 萬種顏色。PNG 圖片具備比 JPG 更強的色彩表現力,對線條的處理更加細膩,對透明度有良好的支持。它彌補了上文咱們提到的 JPG 的侷限性,惟一的 BUG 就是體積太大。
SVG 特色:文本文件、體積小、不失真、兼容性好,SVG(可縮放矢量圖形)是一種基於 XML 語法的圖像格式。它和本文說起的其它圖片種類有着本質的不一樣:SVG 對圖像的處理不是基於像素點,而是是基於對圖像的形狀描述。
Base64 特色:文本文件、依賴編碼、小圖標解決方案,Base64 並不是一種圖片格式,而是一種編碼方式。Base64 和雪碧圖同樣,是做爲小圖標解決方案而存在的。
WebP 特色:年輕的全能型選手,WebP 像 JPEG 同樣對細節豐富的圖片信手拈來,像 PNG 同樣支持透明,像 GIF 同樣能夠顯示動態圖片——它集多種圖片文件格式的優勢於一身。可是畢竟年輕,兼容性存在一些問題。
在客戶端渲染模式下,服務端會把渲染須要的靜態文件發送給客戶端,客戶端加載過來以後,本身在瀏覽器裏跑一遍 JS,根據 JS 的運行結果,生成相應的 DOM。頁面上呈現的內容,你在 html 源文件裏裏找不到——這正是它的特色。
在服務端渲染的模式下,當用戶第一次請求頁面時,由服務器把須要的組件或頁面渲染成HTML字符串,而後把它返回給客戶端。頁面上呈現的內容,咱們在 html 源文件裏也能找到。服務端渲染解決了一個很是關鍵的性能問題——首屏加載速度過慢,也解決了SEO搜索引擎的問題。
瀏覽器的渲染機制通常分爲如下幾個步驟:
在渲染DOM的時候,瀏覽器所作的工做其實是:
CSS 選擇符是從右到左進行匹配的,好比 #myList li {}實際開銷至關高。
CSS 是阻塞的資源。瀏覽器在構建 CSSOM 的過程當中,不會渲染任何已處理的內容。即使 DOM 已經解析完畢了,只要 CSSOM 不 OK,那麼渲染這個事情就不 OK。咱們將 CSS 放在 head 標籤裏 和儘快 啓用 CDN 實現靜態資源加載速度的優化。
JS 引擎是獨立於渲染引擎存在的。咱們的 JS 代碼在文檔的何處插入,就在何處執行。當 HTML 解析器遇到一個 script 標籤時,它會暫停渲染過程,將控制權交給 JS 引擎。JS 引擎對內聯的 JS 代碼會直接執行,對外部 JS 文件還要先獲取到腳本、再進行執行。等 JS 引擎運行完畢,瀏覽器又會把控制權還給渲染引擎,繼續 CSSOM 和 DOM 的構建。
重繪不必定致使迴流,迴流必定會致使重繪。迴流比重繪作的事情更多,帶來的開銷也更大。在開發中,要從代碼層面出發,儘量把迴流和重繪的次數最小化。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>DOM操做測試</title> </head> <body> <div id="container"></div> </body> </html>
for(var count=0;count<10000;count++){ document.getElementById('container').innerHTML+='<span>我是一個小測試</span>' //咱們每一次循環都調用 DOM 接口從新獲取了一次 container 元素,額外開銷 }
進化一:
// 只獲取一次container let container = document.getElementById('container') for(let count=0;count<10000;count++){ container.innerHTML += '<span>我是一個小測試</span>' }
進化二:
//減小沒必要要的DOM更改 let container = document.getElementById('container') let content = '' for(let count=0;count<10000;count++){ // 先對內容進行操做 content += '<span>我是一個小測試</span>' } // 內容處理好了,最後再觸發DOM的更改 container.innerHTML = content
事實上,考慮JS 的運行速度,比 DOM 快得多這個特性。咱們減小 DOM 操做的核心思路,就是讓 JS 去給 DOM 分壓。
在 DOM Fragment 中,DocumentFragment 接口表示一個沒有父級文件的最小文檔對象。它被當作一個輕量版的 Document 使用,用於存儲已排好版的或還沒有打理好格式的XML片斷。由於 DocumentFragment 不是真實 DOM 樹的一部分,它的變化不會引發 DOM 樹的從新渲染的操做(reflow),且不會致使性能等問題。
進化三:
let container = document.getElementById('container') // 建立一個DOM Fragment對象做爲容器 let content = document.createDocumentFragment() for(let count=0;count<10000;count++){ // span此時能夠經過DOM API去建立 let oSpan = document.createElement("span") oSpan.innerHTML = '我是一個小測試' // 像操做真實DOM同樣操做DOM Fragment對象 content.appendChild(oSpan) } // 內容處理好了,最後再觸發真實DOM的更改 container.appendChild(content)
進化四:
當涉及到過萬調數據進行渲染,並且要求不卡住畫面,如何解決?
如何在不卡住頁面的狀況下渲染數據,也就是說不能一次性將幾萬條都渲染出來,而應該一次渲染部分 DOM,那麼就能夠經過 requestAnimationFrame 來每 16 ms 刷新一次。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> </head> <body> <ul> 控件 </ul> <script> setTimeout(() => { // 插入十萬條數據 const total = 100000 // 一次插入 20 條,若是以爲性能很差就減小 const once = 20 // 渲染數據總共須要幾回 const loopCount = total / once let countOfRender = 0 let ul = document.querySelector('ul') function add() { // 優化性能,插入不會形成迴流 const fragment = document.createDocumentFragment() for (let i = 0; i < once; i++) { const li = document.createElement('li') li.innerText = Math.floor(Math.random() * total) fragment.appendChild(li) } ul.appendChild(fragment) countOfRender += 1 loop() } function loop() { if (countOfRender < loopCount) { window.requestAnimationFrame(add) } } loop() }, 0) </script> </body> </html>
window.requestAnimationFrame() 方法告訴瀏覽器您但願執行動畫並請求瀏覽器在下一次重繪以前調用指定的函數來更新動畫。該方法使用一個回調函數做爲參數,這個回調函數會在瀏覽器重繪以前調用。
注意:若您想要在下次重繪時產生另外一個動畫畫面,您的回調例程必須調用 requestAnimationFrame()。
咱們先了解javascript運行機制,對渲染是大有幫助的,能夠看我歷史文章JavaScript運行機制,
Javascript運行機制深刻。
事件循環中的異步隊列有兩種:macro(宏任務)隊列和 micro(微任務)隊列。
常見的 macro-task 好比: setTimeout、setInterval、 setImmediate、script(總體代碼)、 I/O 操做、UI 渲染等。
常見的 micro-task 好比: process.nextTick、Promise、MutationObserver 等。
例子分析:
// task是一個用於修改DOM的回調 setTimeout(task, 0)
上面代碼,如今 task 被推入的 macro 隊列。但由於 script 腳本自己是一個 macro 任務,因此本次執行完 script 腳本以後,下一個步驟就要去處理 micro 隊列了,再往下就去執行了一次 render,必須等待下一次的loop。
Promise.resolve().then(task)
上面代碼,咱們結束了對 script 腳本的執行,是否是緊接着就去處理 micro-task 隊列了?micro-task 處理完,DOM 修改好了,緊接着就能夠走 render 流程了——不須要再消耗多餘的一次渲染,不須要再等待一輪事件循環,直接爲用戶呈現最即時的更新結果。
當咱們須要在異步任務中實現 DOM 修改時,把它包裝成 micro 任務是相對明智的選擇。
上面說了重繪與迴流,Event loop,但不少人不知道的是,重繪和迴流其實和 Event loop 有關。
當用戶進行滾動,觸發scroll事件,用戶的每一次滾動都將觸發咱們的監聽函數。函數執行是吃性能的,頻繁地響應某個事件將形成大量沒必要要的頁面計算。所以,咱們須要針對那些有可能被頻繁觸發的事件做進一步地優化。節流與防抖就頗有必要了!
詳細看歷史文章防抖動與節流