瀏覽器工作原理及web 效能優化(上)

frank26發表於2018-08-16

瀏覽器工作原理

一、瀏覽器簡介

  • 分類:現在主要有五大主流瀏覽器: Chrome, Internet Explorer, Firefox, Safari and Opera.移動端上是Android Browser, iPhone, Opera Mini and Opera Mobile, UC Browser, the Nokia S40/S60 browsers,除了Opera,這些瀏覽器都是基於WebKit核心的(目前可能有變)。
  • 功能 :根據W3C制定的一系列規範,從服務端請求並渲染資源
  • 普遍外觀:位址列,前進後退,書籤,重新整理及取消,主頁
  • 深層結構:下邊主要介紹———渲染引擎及JS引擎
    瀏覽器工作原理及web 效能優化(上)
    • 使用者介面(User Interface) :包括位址列、前進/後退按鈕、書籤選單等。除了瀏覽器主視窗顯示的您請求的頁面外,其他顯示的各個部分都屬於使用者介面。
    • 瀏覽器引擎(Browser engine): 在使用者介面和呈現引擎之間傳送指令。
    • 呈現引擎(Rendering engine): 負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在螢幕上。
    • 網路(Networking) : 用於網路呼叫,比如 HTTP 請求。其介面與平臺無關,併為所有平臺提供底層實現。
    • 使用者介面後端(UI backend):用於繪製基本的視窗小部件,比如組合框和視窗。其公開了與平臺無關的通用介面,而在底層使用作業系統的使用者介面方法。
    • JavaScript 直譯器(JS Interpreter):用於解析和執行 JavaScript 程式碼。
    • 資料儲存(Datapersistence):這是持久層。瀏覽器需要在硬碟上儲存各種資料,例如 Cookie。新的 HTML 規範 (HTML5) 定義了“網路資料庫”,這是一個完整(但是輕便)的瀏覽器內資料庫。

二、渲染引擎

  • 分類:不同瀏覽器用不同渲染引擎。

Internet Explorer uses Trident, Firefox uses Gecko, Safari uses WebKit. Chrome and Opera (from version 15) use Blink, a fork of WebKit.WebKit 是一個開源渲染引擎,起初作為Linux platform的引擎,後被 Apple應用於Mac. 詳見 webkit.org.

  • 載入:

    • 瀏覽器根據 DNS 伺服器得到域名的 IP 地址
    • 向這個 IP 的機器傳送 HTTP 請求
    • 伺服器收到、處理並返回 HTTP 請求
    • 瀏覽器得到返回內容

客戶端:如在瀏覽器輸入https://www.baidu.com/,經過 DNS 解析,查到baidu.com對應的 IP(不同時間、地點對應的 IP 可能會不同),瀏覽器向該 IP 傳送 HTTP 請求。

服務端:伺服器接收到 HTTP 請求,經計算,返回 HTTP 請求

  • 過程如下:
    瀏覽器工作原理及web 效能優化(上)
    在Chrome開發者工具中的Timeline中可以詳細看到具體過程。

瀏覽器工作原理及web 效能優化(上)

  • 內容如下:

瀏覽器工作原理及web 效能優化(上)

  • 渲染:

瀏覽器對請求的呈現。預設渲染引擎可以呈現html,xml及圖片。(通過外掛)也可以呈現其它資料,比如pdf等。 目前只考慮html和css方面。

渲染主流程:

瀏覽器工作原理及web 效能優化(上)

Figure : Rendering engine basic flow

瀏覽器工作原理及web 效能優化(上)

Figure : WebKit main flow

content tree:在此,渲染引擎解析html文件並將元素轉換成DOM節點。

render tree:在此,渲染引擎解析style(外部css檔案或內聯style)並轉換。

這是一個漸進式過程. 為保證好的UED,渲染引擎儘早展現內容. 在開始構建並展現render樹之前,它不會等所有html被解析。部分內容被解析並呈現,同時程式繼續解析網路中不斷請求到的其餘內容。

具體渲染過程
  • 根據 HTML 結構生成 DOM 樹
  • 根據 CSS 生成 CSSOM
  • 將 DOM 和 CSSOM 整合形成 RenderTree
  • 根據 RenderTree 開始渲染和展示
  • 遇到<script>時,會執行並阻塞渲染

瀏覽器拿到了 server 端返回的 HTML 內容後,開始解析並渲染。最初拿到的內容就是一堆字串,必須先結構化成計算機擅長處理的基本資料結構--- DOM 樹 (最基本的資料結構之一)。

解析過程中,如果遇到<link href="..."><script src="...">這種外鏈載入 CSS 和 JS 的標籤,瀏覽器會非同步下載,下載過程和上文中下載 HTML 的流程一樣。只不過,這裡下載下來的字串是 CSS 或者 JS 格式的。實際應用中,常用媒體型別(media type)和媒體查詢(media query)來解除對渲染的阻塞。

<link href="index.css" rel="stylesheet"> <--阻塞-->
<link href="print.css" rel="stylesheet" media="print"><--載入&& !阻塞,僅在print時實用-->
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)"><--符合條件時阻塞-->
複製程式碼
  • 檢視下邊一段程式碼:
<p>根據 HTML 結構生成 DOM 樹,稍等。。。</p>
<script src="app.js"></script>
<p>根據 CSS 生成 CSSOM,稍等。。。</p>
<script>console.log("inline")</script>
<p>將 DOM 和 CSSOM 整合形成 RenderTree</p>
複製程式碼

這段html被解析時會被js程式碼阻塞(載入+執行),因此常把css放在頭部(保證渲染),把js放在底部(保證非阻塞)。

  • Normal execution <script> 瀏覽器預設:當執行script時解析html程式碼暫停。對於慢服務和重script的情況意味著webpage呈現將被延後。
  • Deferred execution <script defer> 簡而言之:推遲script執行直到html解析結束。該屬性的好處就是DOM渲染友好,對於你的script。然而,並非每個瀏覽器支援該屬性,故不要指望它!
  • Asynchronous execution <script async> 不用管script何時好?async對於兩個都是最好的:html解析可能持續且script將被執行一旦ready。對script標籤推薦這個屬性,如google analytics所分析。
document.createElement("script").async
// true
複製程式碼
<script src="app1.js" defer></script>
<script src="app2.js" defer></script>

<script src="init1.js" async></script>
<script src="init2.js" async></script>
複製程式碼
  • 瀏覽器的容錯機制

您在瀏覽 HTML 網頁時從來不會看到“語法無效”的錯誤。這是因為瀏覽器會糾正任何無效內容,然後繼續工作。

1. 明顯不能在某些外部標籤中新增的元素。在此情況下,我們應該關閉所有標籤,直到出現禁止新增的元素,然後再加入該元素。
2. 我們不能直接新增的元素。這很可能是網頁作者忘記新增了其中的一些標籤(或者其中的標籤是可選的)。這些標籤可能包括:HTML HEAD BODY TBODY TR TD LI(還有遺漏的嗎?)。
3. 向 inline 元素內新增 block 元素。關閉所有 inline 元素,直到出現下一個較高階的 block 元素。
4. 如果這樣仍然無效,可關閉所有元素,直到可以新增元素為止,或者忽略該標記。
複製程式碼

CSS 解析

和 HTML 不同,CSS 是上下文無關的語法,可以使用簡介中描述的各種解析器進行解析。事實上,CSS 規範定義了 CSS 的詞法和語法。

  • WebKit CSS 解析器 WebKit 使用 Flex 和 Bison 解析器生成器,通過 CSS 語法檔案自動建立解析器。正如我們之前在解析器簡介中所說,Bison 會建立自下而上的移位歸約解析器。Firefox 使用的是人工編寫的自上而下的解析器。這兩種解析器都會將 CSS 檔案解析成 StyleSheet 物件,且每個物件都包含 CSS 規則。CSS 規則物件則包含選擇器和宣告物件,以及其他與 CSS 語法對應的物件。
    瀏覽器工作原理及web 效能優化(上)
圖:解析 CSS

處理指令碼和樣式表的順序

  • 指令碼

:遇到 <script>文件的解析將停止,直到指令碼執行完畢。如果指令碼是外部的,那麼解析過程會停止,直到從網路同步抓取資源完成後再繼續。HTML5 增加了一個選項----“defer”,這樣不會停止文件解析,而是等到解析結束才執行.

  • 預解析 WebKit 和 Firefox 都進行了這項優化。**在執行指令碼時,其他執行緒會解析文件的其餘部分,找出並載入需要通過網路載入的其他資源。**通過這種方式,資源可以在並行連線上載入,從而提高總體速度。請注意,預解析器不會修改 DOM 樹,而是將這項工作交由主解析器處理;預解析器只會解析外部資源(例如外部指令碼、樣式表和圖片)的引用。

  • 樣式表: 理論上來說,應用樣式表不會更改 DOM 樹,因此似乎沒有必要等待樣式表並停止文件解析

  • render樹構建: 在 DOM 樹構建的同時,瀏覽器還會構建:render樹---按照正確的順序繪製內容。

  • render樹和 DOM 樹的關係:呈現器是和 DOM 元素相對應的,但並非一一對應。非視覺化的 DOM 元素不會插入呈現樹中,例如“head”元素。如果元素的 display 屬性值為“none”,那麼也不會顯示在呈現樹中(但是 visibility 屬性值為“hidden”的元素仍會顯示)。

Web 安全

常見的web攻擊方式:

  • SQL 注入:輸入時進行了惡意的 SQL 拼裝,導致最後生成的 SQL 有問題。攻擊方式低端,常出現在比較低端的系統。

  • XSS(Cross Site Scripting,跨站指令碼攻擊)

前端最常見的攻擊方式,很多網站(如 阮一峰部落格)都被 XSS 攻擊過。

原理:通過某種方式(釋出文章、釋出評論等)將一段特定的 JS 程式碼隱蔽地輸入進去。JS 程式碼一旦執行,那可就不受控制了,因為它跟網頁原有的 JS 有同樣的許可權,例如可以獲取 server 端資料、可以獲取 cookie 等。於是,攻擊就這樣發生了。

預防:最根本的方式,用正規表示式進行轉換處理,如:

< 替換為:&lt;
> 替換為:&gt;
& 替換為:&amp;
‘ 替換為:&#x27;
” 替換為:&quot;
複製程式碼

另外,還可以通過對 cookie控制,比如對敏感的 cookie 增加http-only限制,讓 JS 獲取不到 cookie 的內容。

  • CSRF(Cross-site request forgery,跨站請求偽造) 理論基礎:藉助了一個 cookie的特性,劫持操作者的許可權來偷偷地完成某個操作,而不是拿到使用者的資訊。

預防 :加入各個層級的許可權驗證,如涉及現金交易,必須要輸入密碼或者指紋才行等。

瀏覽器重新渲染(re-render):重繪(repaint)與重排(reflow)

重新渲染,就需要重新生成佈局和重新繪製。前者叫做"重排"(reflow),後者叫做"重繪"(repaint)。"重繪"不一定需要"重排","重排"必然導致"重繪"。重排和重繪會不斷觸發,這是不可避免的。但是,它們非常耗費資源,是導致網頁效能低下的根本原因。提高網頁效能,就是要降低"重排"和"重繪"的頻率和成本,儘量少觸發重新渲染。

  • 觸發重新渲染的常見情況:
    • 修改DOM
    • 修改樣式表
    • 使用者事件(比如滑鼠懸停、頁面滾動、輸入框鍵入文字、改變視窗大小等等)
    • 訪問元素屬性:
      • offsetTop/offsetLeft/offsetWidth/offsetHeight
      • scrollTop/scrollLeft/scrollWidth/scrollHeight
      • clientTop/clientLeft/clientWidth/clientHeight
      • getComputedStyle()
  • 提高效能的技巧:
    • css:

      • display(none->block)
      • position(absolute,fixed)
      • visibility(hidden)
    • js:

      • 使用優秀類庫的Virtual DOM(React,Vue)
      • 注意作用域
        • 避免全域性查詢
        • 避免with
      • 選擇正確的方法
        • 避免沒必要的查詢(效能部分的一部分是和解決問題的演算法和方法相關的)
        • 優化迴圈
        • 展開迴圈
        • 避免雙重解釋
        • 其它
          • 原生方法較快:
          • switch語句較快
          • 位運算子較快
      • 最小化語句數
        • 多個變數宣告
        • 插入迭代值
        • 使用陣列和物件字面量
      • 優化DOM互動(如下)
    • BOM:

      • window.requestAnimationFrame
      • window.requestIdleCallback
    • DOM:

      • document.createDocumentFragment:最小化現場更新
      • innerHTML:要不使用標準DOM方法建立同樣的DOM快得多(內部方法是編譯好而非解釋執行的)。
      • 事件代理:頁面處理程式的數量和頁面響應UED速度負相關。事件處理程式本質上是一種函式,是一種物件,存放在記憶體中,設定大量的事件處理程式會使記憶體中的物件變多,Web 程式的效能會變得越來越差,使用者體驗很不好。
      • 注意HTMLCollection:最小化對它們的訪問,以下會返回HTMLCollection物件
        • getElementByTagName
        • childNodes
        • attributes
        • 特殊集合:document.forms,document.images etc.
      • cloneNode
      • 集中DOM的讀寫操作
      • 使用 SSR 後端渲染,資料直接輸出到 HTML 中,減少瀏覽器使用 JS 模板渲染頁面 HTML 的時間
    • HTTP: 減少頁面體積,提升網路載入

      • 靜態資源的壓縮合並(JS 程式碼壓縮合並、CSS 程式碼壓縮合並、雪碧圖)
      • 靜態資源快取(資源名稱加 MD5 戳)
      • 使用 CDN 讓資源載入更快

應用示例

  • js中應用到的簡單演算法

標記名稱 描述
O(1) 常數:不管有多少值,執行時間恆定,一般表示簡單值和儲存在變數中的值(陣列元素訪問)
O(logn) 對數:執行總時間與數量相關,但要完成演算法並不一定獲取每個值。如二分查詢
O(n) 線性:執行總時間和數量直接相關。如遍歷操作
O(n2) 平方:執行總時間與數量有關,每個值至少要獲取n次。如插入排序
  • 程式碼示例

  1. 避免全域性查詢

測試網頁

// bad(3次全域性查詢) 
function updateUI(){
console.time()
	var images=document.getElementsByTagName('img')
for(var i=0,len=images.length;i<len;i++){
	images[i].title=document.title+'image'+i;
}
return console.timeEnd()
}
// default: 0.5439453125ms
// good(一次全域性查詢)
function updateUI(){
console.time()
var doc = document
	var images=doc.getElementsByClassName('img')
for(var i=0,len=images.length;i<len;i++){
	images[i].title=doc.title+'image'+i;
}
return console.timeEnd()
}
// default: 0.120849609375ms
複製程式碼

2.避免不必要的屬性查詢(見JS常見演算法型別) 最簡單最便捷的演算法是常數O(1)

// 四次查詢(5,value,10,sum)
var value = 5
var sum = value + 10 
console.log(sum)
// default: 0.375ms
複製程式碼

3.使用變數和陣列要比訪問物件上屬性(O(n))更高效。

// good
var values = [1, 2, 3]
var sum = values[0] + values[1]
console.log(sum)
// default: 0.298095703125ms
// bad
var values = {a:1, b:2}
var sum = values.a + values.b
console.log(sum)
// default: 0.302978515625ms
複製程式碼

4.⚠️注意獲取單個值的屬性查詢

// bad
var query = window.location.href.substring(window.location.href.indexOf('?'))
// default: 0.02783203125ms
// good
var url = window.location.href
var query = url.substring(url.indexOf('?'))
// default: 0.02001953125ms
複製程式碼
  1. 優化迴圈
// bad
var values=new Array().fill(10000)
for(var i=0;i<values.length;i++){i}
// default: 0.101806640625ms
// good(使用減值迭代:將終止條件從values.length的O(n)呼叫簡化成了O(1)呼叫)
for(var i = values.length-1;i>0;i--){}
// default: 0.01708984375ms (一個數量級)
// good(後測試迴圈:迴圈部分已完全優化,任何進一步的優化只能在處理語句中進行了),這種寫法的弊端就是可讀性很差
var i = values.length - 1
if(i > -1){
    do{
        //
    }while(--i >= 0)
}
// default: 0.011962890625ms
複製程式碼

6.避免雙重解釋(JS 程式碼執行的同時必須新啟動一個解析器來解析新的程式碼。例項化一個新的解析器有不容忽視的開銷)

eval("alert('hi')")// bad
var say = new Function("alert('hi')")// bad
setTimeout('alert("hi")',500)// bad
alert('hi')// good
var say = function (){
    alert('hi')
}// good
setTimeout(function (){
    alert('hi')
},500)// good
複製程式碼
  1. 最小化語句數
// 多個變數宣告
var count = 0,
    color = 'red',
    values = [],
    now = new Date()
// 插入迭代值
var value = values[i]; i ++ // bad
var value = values[i++] // good
// 使用陣列和物件字面量
var values = new Array(); values[0] = 1; values[1] = 2 // bad(3個語句)
var person = new Object();
person.a = 1;
person.b = 2 // bad(3個語句)
var values = [1,2] // good(2個語句建立和初始化陣列)
var person = {a:1,b:2}// good(2個語句建立和初始化物件)
複製程式碼
  1. 優化DOM互動(因DOM內部包含了非常多的屬性及方法,DOM 互動非常耗時)
// DOM裡的內容
var div = document.createElement('div'), result =  ''
for(var i in div){
    result += i + '  '
}
/*
"align  title  lang  translate  dir  dataset  hidden  tabIndex  accessKey  draggable  spellcheck  autocapitalize  contentEditable  isContentEditable  inputMode  offsetParent  offsetTop  offsetLeft  offsetWidth  offsetHeight  style  innerText  outerText  onabort  onblur  oncancel  oncanplay  oncanplaythrough  onchange  onclick  onclose  oncontextmenu  oncuechange  ondblclick  ondrag  ondragend  ondragenter  ondragleave  ondragover  ondragstart  ondrop  ondurationchange  onemptied  onended  onerror  onfocus  oninput  oninvalid  onkeydown  onkeypress  onkeyup  onload  onloadeddata  onloadedmetadata  onloadstart  onmousedown  onmouseenter  onmouseleave  onmousemove  onmouseout  onmouseover  onmouseup  onmousewheel  onpause  onplay  onplaying  onprogress  onratechange  onreset  onresize  onscroll  onseeked  onseeking  onselect  onstalled  onsubmit  onsuspend  ontimeupdate  ontoggle  onvolumechange  onwaiting  onwheel  onauxclick  ongotpointercapture  onlostpointercapture  onpointerdown  onpointermove  onpointerup  onpointercancel  onpointerover  onpointerout  onpointerenter  onpointerleave  nonce  click  focus  blur  namespaceURI  prefix  localName  tagName  id  className  classList  slot  attributes  shadowRoot  assignedSlot  innerHTML  outerHTML  scrollTop  scrollLeft  scrollWidth  scrollHeight  clientTop  clientLeft  clientWidth  clientHeight  onbeforecopy  onbeforecut  onbeforepaste  oncopy  oncut  onpaste  onsearch  onselectstart  previousElementSibling  nextElementSibling  children  firstElementChild  lastElementChild  childElementCount  onwebkitfullscreenchange  onwebkitfullscreenerror  setPointerCapture  releasePointerCapture  hasPointerCapture  hasAttributes  getAttributeNames  getAttribute  getAttributeNS  setAttribute  setAttributeNS  removeAttribute  removeAttributeNS  hasAttribute  hasAttributeNS  getAttributeNode  getAttributeNodeNS  setAttributeNode  setAttributeNodeNS  removeAttributeNode  closest  matches  webkitMatchesSelector  attachShadow  getElementsByTagName  getElementsByTagNameNS  getElementsByClassName  insertAdjacentElement  insertAdjacentText  insertAdjacentHTML  requestPointerLock  getClientRects  getBoundingClientRect  scrollIntoView  scrollIntoViewIfNeeded  animate  before  after  replaceWith  remove  prepend  append  querySelector  querySelectorAll  webkitRequestFullScreen  webkitRequestFullscreen  attributeStyleMap  scroll  scrollTo  scrollBy  createShadowRoot  getDestinationInsertionPoints  computedStyleMap  ELEMENT_NODE  ATTRIBUTE_NODE  TEXT_NODE  CDATA_SECTION_NODE  ENTITY_REFERENCE_NODE  ENTITY_NODE  PROCESSING_INSTRUCTION_NODE  COMMENT_NODE  DOCUMENT_NODE  DOCUMENT_TYPE_NODE  DOCUMENT_FRAGMENT_NODE  NOTATION_NODE  DOCUMENT_POSITION_DISCONNECTED  DOCUMENT_POSITION_PRECEDING  DOCUMENT_POSITION_FOLLOWING  DOCUMENT_POSITION_CONTAINS  DOCUMENT_POSITION_CONTAINED_BY  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC  nodeType  nodeName  baseURI  isConnected  ownerDocument  parentNode  parentElement  childNodes  firstChild  lastChild  previousSibling  nextSibling  nodeValue  textContent  hasChildNodes  getRootNode  normalize  cloneNode  isEqualNode  isSameNode  compareDocumentPosition  contains  lookupPrefix  lookupNamespaceURI  isDefaultNamespace  insertBefore  appendChild  replaceChild  removeChild  addEventListener  removeEventListener  dispatchEvent  "
*/
複製程式碼
  1. 最小化現場更新
// bad
console.time()
var list = document.createElement('ul'),
    item,
    i
for(i=0;i<10000;i++){
    item = document.createElement('li')
    list.appendChild(item)
    item.appendChild(document.createTextNode('item' + i))
}
document.body.appendChild(list)
console.timeEnd()
// default: 26.902099609375ms

// good
console.time()
var list = document.createElement('ul'),
    fragment = document.createDocumentFragment(),
    item,
    i
for(i=0;i<10000;i++){
    item = document.createElement('li')
    fragment.appendChild(item)
    item.appendChild(document.createTextNode('item' + i))
}
document.body.appendChild(fragment)
console.timeEnd()
// VM7017:12 default: 29.4169921875ms ???
// good (使用innerHTML(編譯好的而非解釋執行的))
console.time()
var list = document.createElement('ul'),
    html = '',
    i
for(i=0;i<10000;i++){
    // list.innerHTML += "<li>Item " + i + "</li>" // 避免
   html += "<li>Item " + i + "</li>"
}
list.innerHTML = html
document.body.appendChild(list)
console.timeEnd()
//default: 25.760986328125ms
複製程式碼
  1. 最小化對HTMLColletion的訪問
console.time()
var images = document.getElementsByTagName('img'),
i,
image,
len,
temp
for(i = 0, len=images.length;i<len;i++){
    image = images[i]// 儲存當前image,避免了對images的訪問
	temp=image+Image
}
console.timeEnd()
// default: 0.06591796875ms
console.time()
var images = document.getElementsByTagName('img'),
i,
len,
temp
for(i = 0, len=images.length;i<len;i++){
	temp=images[i]+images[i]
}
console.timeEnd()
// VM6253:9 default: 0.115966796875ms
複製程式碼

靜態資源快取

通過連結名稱控制快取

<script src="a.js"></script>
複製程式碼

只有內容改變的時候,連結名稱才會改變

<script src="b.js"></script>
複製程式碼

可通過前端構建工具根據檔案內容,為檔名稱新增 MD5 字尾。

使用CDN(專業的載入優化方案)讓資源載入更快

<script src="https://cdn.bootcss.com/zepto/1.0rc1/zepto.min.js"></script>
複製程式碼

事件節流:函式節流(throttle)與函式去抖(debounce)

  • 概念:
    • 函式節流:如果將水龍頭擰緊直到水是以水滴的形式流出,那你會發現每隔一段時間,就會有一滴水流出。也就是說會預先設定一個執行週期,當呼叫動作的時刻大於等於執行週期則執行該動作,然後進入下一個新週期。
    • 函式去抖:如果用手指一直按住一個彈簧(debounce),它將不會彈起直到你鬆手為止。也就是說當呼叫動作n毫秒後,才會執行該動作,若在這n毫秒內又呼叫此動作則將重新計算執行時間。
  • 為什麼要節流?

由於事件頻繁被觸發,因而頻繁執行DOM操作、資源載入等重行為,導致UI停頓甚至瀏覽器崩潰。

  • 何時考慮節流?

    • window物件的resize、scroll事件;
    • 拖拽時的mousemove事件;
    • 遊戲中的mousedown、keydown、mouseenter,mouseover事件;
    • 文字輸入input、blur、自動完成的keyup事件等。
  • 具體做法:

// throttle
var throttle = function(delay, action){
  var last = 0return function(){
    var curr = +new Date()
    if (curr - last > delay){
      action.apply(this, arguments)
      last = curr 
    }
  }
}
// debounce
var textarea = document.getElementById('btn')
var timeoutId
textarea.addEventListener('keyup', function () {
    if (timeoutId) {
        clearTimeout(timeoutId)
    }
    timeoutId = setTimeout(function () {
        // 觸發 change 事件
    }, 100)
})
複製程式碼

web效能優化(web performance)

為什麼要效能優化

  • 留住使用者(Performance is about retaining users)
  • 提升轉化率(Performance is about improving conversions)
    瀏覽器工作原理及web 效能優化(上)
轉化率就是金錢
  • 關乎使用者體驗(Performance is about the user experience)
  • 以人為本(Performance is about people)

如何去優化

要傳送什麼(Mind what resources you send)

構建高效能應用程式的有效方法是稽核傳送給使用者的資源。雖然Chrome開發人員工具中的網路皮膚可以很好地總結給定頁面上使用的所有資源,但如果您到目前為止尚未考慮效能,那麼知道從哪裡開始可能會令人生畏。以下是一些建議:

  • 如果您使用Bootstrap或Foundation來構建UI,請問自己是否有必要。這些抽象新增了瀏覽器必須下載,解析和應用於頁面的大量CSS,所有這些都是在特定於站點的CSS進入圖片之前。 Flexbox和Grid在使用相對較少的程式碼建立簡單和複雜佈局方面非常出色。由於CSS是一種render阻塞資源,因此CSS框架的開銷可能會顯著延遲渲染。您應該通過消除不必要的開銷來尋求加速渲染,而非儘可能依賴於瀏覽器中的工具。

  • JavaScript庫很方便,但並不總是必要的。以jQuery為例:由於querySelectorquerySelectorAll等方法,元素選擇得到了極大的簡化。使用addEventListener可以輕鬆進行事件繫結。classList,setAttributegetAttribute提供了使用類和元素屬性的簡便方法。如果你必須使用類庫,研究更精簡的替代品。例如,Zepto是一個較小的jQuery替代品,Preact是React的一個小得多的替代品。

  • 並非所有網站都需要是單頁面應用程式(SPA),因為它們經常廣泛使用JavaScript。 JavaScript是我們在位元組的web位元組上提供的最昂貴的資源,因為它不僅必須下載,還必須解析,編譯和執行。例如,具有優化前端架構的新聞和部落格站點可以像傳統的多頁體驗一樣表現良好。

如何傳送(Mind how you send resources)

當您知道需要為您的應用傳送哪些資源以使其成為您想要的美觀和功能時,請考慮下一步如何傳送它們。與預見和預防一樣,交付對於構建快速使用者體驗至關重要。

  • 遷移到HTTP / 2。 HTTP / 2解決了HTTP / 1.1中固有的許多效能問題,例如併發請求限制和缺少頭壓縮。
  • 使用資源提示加快資源交付。 rel = preload是一個這樣的資源提示,它允許在瀏覽器發現它們之前提前獲取關鍵資源。這可以對頁面呈現產生明顯的積極影響,並在明智地使用時降低互動時間。 rel = preconnect是另一個資源提示,可以掩蓋為第三方域上託管的資源開啟新連線的延遲。
  • 現代網站平均提供大量JavaScript和CSS。在HTTP / 1環境中將樣式和指令碼捆綁到大型捆綁包中很常見。這樣做是因為大量請求對效能有害。現在不再是HTTP / 2在場的情況,因為多個同時請求更便宜。考慮在webpack中使用程式碼拆分來限制下載的指令碼數量,使其僅限於當前頁面或檢視所需的內容。將CSS分成較小的模板或特定於元件的檔案,並僅包含可能使用它們的資源。

資料規模(Mind how much data you send)

通過一些關於哪些資源適合傳送以及如何傳送它們的想法,我們將介紹一些限制傳送資料的建議:

  1. 配置伺服器以壓縮資源。壓縮會大大減少您傳送給使用者的資料量,尤其是在涉及文字資產的情況下。 GZIP在這個領域是一種非常優秀的格式,但Brotli壓縮可以更進一步。但是,要理解壓縮並不是效能問題的全部:一些隱式壓縮的檔案格式(例如,JPEG,PNG,GIF,WOFF等)不響應壓縮,因為它們已經被壓縮。
  2. 優化圖片以確保您的網站儘可能少地傳送圖片資料。由於圖片構成了網路上每頁平均有效負載的很大一部分,因此圖片優化代表了提升效能的獨特機會。
  3. 客戶端提示可用於根據當前網路條件和裝置特徵定製資源交付。 DPR,Width和Viewport-Width標頭可以幫助您使用伺服器端程式碼為裝置提供最佳影象,並提供更少的標記。 Save-Data標頭可以幫助您為明確要求您的使用者提供更輕鬆的應用程式體驗。
  4. NetworkInformation API公開有關使用者網路連線的資訊。此資訊可用於修改較慢網路上的使用者的應用程式體驗。
  5. 減少請求(HTTP requests reduction)
  6. 檔案壓縮(File compression)
  7. 優化web快取(Web caching optimization)

Web Caching Optimization reduces server load, bandwidth usage, and latency. CDNs use dedicated web caching software to store copies of documents passing through their system. Leveraging the browser cache is crucial. It is recommended to have a max-age of 7 days in such cases. This saves server time and makes things altogether faster.

  1. 簡化程式碼(Code minification)
  2. 向量圖替換點陣圖(Replacement of vector graphics)
  3. 避免301 重定向

Redirects are performance killers. Avoid them whenever possible. A redirect will generate additional round-trip times and therefore quickly doubles the time that is required to load the initial HTML document before the browser even starts to load other assets.

  1. 採用基於雲的網站監控(Adopt Cloud-based Website Monitoring)

  2. 資源預載入:Pre-fetching是一種提示瀏覽器預先載入使用者之後可能會使用到的資源的方法。

  • 使用dns-prefetch來提前進行DNS解析,以便之後可以快速地訪問另一個主機名(瀏覽器會在載入網頁時對網頁中的域名進行解析快取,這樣你在之後的訪問時無需進行額外的DNS解析,減少了使用者等待時間,提高了頁面載入速度)。
<link rel="dns-prefetch" href="other.hostname.com">
複製程式碼
  • 使用prefetch屬性可以預先下載資源,不過它的優先順序是最低的。
<link rel="prefetch"  href="/some_other_resource.jpeg">
複製程式碼
  • Chrome允許使用subresource屬性指定優先順序最高的下載資源(當所有屬性為subresource的資源下載完完畢後,才會開始下載屬性為prefetch的資源)。
<link rel="subresource"  href="/some_other_resource.js">
複製程式碼
  • prerender可以預先渲染好頁面並隱藏起來,之後開啟這個頁面會跳過渲染階段直接呈現在使用者面前(推薦對使用者接下來必須訪問的頁面進行預渲染,否則得不償失)。
<link rel="prerender"  href="//domain.com/next_page.html">
複製程式碼
  1. SSL certificate/ HTTPS

  2. 熱連結保護(Hotlink protection) 限制HTTP引用,以防止他人將您的資源嵌入其他網站。 熱連結保護將通過禁止其他網站顯示您的影象來節省頻寬。

  3. 基礎設施(Infrastructure)

  4. 資料庫優化(Database Optimization)

  5. 使用webworker

(前端)主流框架主流做法案例(見下一篇)

參考連結

相關文章