對于前端性能優(yōu)化的理解與實踐

先從一道題目說起

從輸入 URL 到頁面加載完成,發(fā)生了什么?

  • 站在性能優(yōu)化的角度;我們可以分為5個過程;
  1. DNS 解析
  2. TCP 連接
  3. HTTP 請求拋出
  4. 服務端處理請求,HTTP 響應返回
  5. 瀏覽器拿到響應數(shù)據(jù),解析響應內容,把解析的結果展示給用戶
  • 我們從這五個過程個個擊破;
    dns解析花時間,tcp連接慢;這些需要我們的服務端解決;
    那么我們的前端工程師在HTTP請求或者瀏覽器端能做一些什么優(yōu)化呢?
    http方面前端可以減少請求次數(shù),壓縮體積;
    瀏覽器前端可以做的事情比較多了,例如資源加載優(yōu)化、服務端渲染、瀏覽器緩存機制的利用、DOM 樹的構建、網頁排版和渲染過程、回流與重繪的考量、DOM 操作的合理規(guī)避


    1669f5358f63c0f8 (1).png

網絡層面(http請求優(yōu)化and減少網絡請求)

webpack打包體積優(yōu)化
  • webpack-bundle-analyzer 是一款包可視化工具,可以找出體積大的模塊;
  • 刪除冗余代碼 webpack3可以使用UglifyJsPlugin ;webpack4已經自帶了,只需要配置下;
  • 按需加載 vue項目可以用require.ensure來實現(xiàn)
  • gzip 本來是服務端的工作,webpack也有gzip可以幫助服務端減輕壓力
圖片
  • JPEG/JPG:有損壓縮、體積小、加載快、不支持透明;龐大的圖片用jpg
  • PNG-8 與 PNG-24:無損壓縮、質量高、體積大、支持透明 像logo類等比較突出的最好用png
  • SVG (字體圖標):文本文件、體積小、不失真、兼容性好
  • Base64 :文本文件、依賴編碼、小圖標解決方案,Base64 是作為雪碧圖的補充而存在的;Base64 編碼后,圖片大小會膨脹為原文件的 4/3;在傳輸非常小的圖片的時候,Base64 帶來的文件體積膨脹、以及瀏覽器解析 Base64 的時間開銷,與它節(jié)省掉的 HTTP 請求開銷相比,可以忽略不計
  • CSS Sprites(精靈圖/雪碧圖):小圖標解決方案
  • WebP :與 PNG 相比,WebP 無損圖像的尺寸縮小了 26%。限制WebP發(fā)展的是瀏覽器兼容問題;
瀏覽器緩存
  • Memory Cache 內存緩存是快的,也是“短命”的。
  • Service Worker Cache 幫我們實現(xiàn)離線緩存、消息推送和網絡代理等功能,但必需以https 協(xié)議為前提
  • Push Cache HTTP2存在,Push Cache 是緩存的最后一道防線,會話階段的緩存;
  • HTTP Cache (主要、最具有代表性的)

HTTP緩存分為強緩存和協(xié)商緩存
強緩存:Expires 和 Cache-Control (http1.1新增)兩個字段來控制
expires 能做的事情,Cache-Control 都能做;expires 完成不了的事情,Cache-Control 也能做。因此,Cache-Control 可以視作是 expires 的完全替代方案。Cache-Control 相對于 expires 更加準確,它的優(yōu)先級也更高。當 Cache-Control 與 expires 同時出現(xiàn)時,我們以 Cache-Control 為準。

public 與 private 是針對資源是否能夠被代理服務緩存而存在的一組對立概念。

no-cache 繞開了瀏覽器:我們?yōu)橘Y源設置了 no-cache 后,每一次發(fā)起請求都不會再去詢問瀏覽器的緩存情況,而是直接向服務端去確認該資源是否過期;no-store 比較絕情,顧名思義就是不使用任何緩存策略。在 no-cache 的基礎上,它連服務端的緩存確認也繞開了,只允許你直接向服務端發(fā)送請求、并下載完整的響應。

協(xié)商緩存:協(xié)商緩存機制下,瀏覽器需要向服務器去詢問緩存的相關信息,進而判斷是重新發(fā)起請求、下載完整的響應,還是從本地獲取緩存的資源。資源會被重定向到瀏覽器緩存,這種情況下網絡請求對應的狀態(tài)碼是 304

165f701820fafcf8 (1).png

當我們的資源內容不可復用時,直接為 Cache-Control 設置 no-store,拒絕一切形式的緩存;否則考慮是否每次都需要向服務器進行緩存有效確認,如果需要,那么設 Cache-Control 的值為 no-cache;否則考慮該資源是否可以被代理服務器緩存,根據(jù)其結果決定是設置為 private 還是 public;然后考慮該資源的過期時間,設置對應的 max-age 和 s-maxage 值;最后,配置協(xié)商緩存需要用到的 Etag、Last-Modified 等參數(shù)。

本地存儲

  • cookie 只能存儲4KB ,緊跟域名的
  • Web Storage Local Storage和Session Storage 這兩個對前端來說很熟悉了;
  • IndexDB 運行在瀏覽器上的非關系型數(shù)據(jù)庫;IndexDB 是沒有存儲上限的(一般來說不會小于 250M)

cdn

  • 內容分發(fā)網絡
  • 緩存和回源。

緩存”就是說我們把資源 copy 一份到 CDN 服務器上這個過程,“回源”就是說 CDN 發(fā)現(xiàn)自己沒有這個資源(一般是緩存的數(shù)據(jù)過期了),轉頭向根服務器(指業(yè)務服務器)或者它的上層服務器去要這個資源的過程。

  • CDN 往往被用來存放靜態(tài)資源

所謂“靜態(tài)資源”,就是像 JS、CSS、圖片等不需要業(yè)務服務器進行計算即得的資源。而“動態(tài)資源”,顧名思義是需要后端實時動態(tài)生成的資源,較為常見的就是 JSP、ASP 或者依賴服務端渲染得到的 HTML 頁面。

  • 性能優(yōu)化方面的應用

同一個域名下的請求會不分青紅皂白地攜帶 Cookie,而靜態(tài)資源往往并不需要 Cookie 攜帶什么認證信息。把靜態(tài)資源和主頁面置于不同的域名下,完美地避免了不必要的 Cookie 的出現(xiàn)!

服務端渲染(SSR)

  • 客戶端渲染:需要把js文件跑完,生成對應的dom樹;
  • 服務端渲染:直接拿到服務端放回的html就可以呈現(xiàn)在用戶面前
  • 質上是本該瀏覽器做的事情,分擔給服務器去做。這樣當資源抵達瀏覽器時,它呈現(xiàn)的速度就快了。

CSSOM,JS的優(yōu)化

瀏覽器背后的運行機制
QQ截圖20181123134723.png
165f7018d20fafcf8 (1).png
CSS 選擇符是從右到左進行匹配的

避免使用通配符,只對需要用到的元素進行選擇。
關注可以通過繼承實現(xiàn)的屬性,避免重復匹配重復定義。
少用標簽選擇器。如果可以,用類選擇器替代
不要畫蛇添足,id 和 class 選擇器不應該被多余的標簽選擇器拖后腿。
減少嵌套。后代選擇器的開銷是最高的,因此我們應該盡量將選擇器的深度降到最低

JS的加載方式

正常模式 <script src="index.js"></script>
async 模式 <script async src="index.js"></script>
defer 模式 <script defer src="index.js"></script>
defer 模式下,JS 的加載是異步的,執(zhí)行是被推遲的。等整個文檔解析完成、DOMContentLoaded 事件即將被觸發(fā)時,被標記了 defer 的 JS 文件才會開始依次執(zhí)行。
從應用的角度來說,一般當我們的腳本與 DOM 元素和其它腳本之間的依賴關系不強時,我們會選用 async;當腳本依賴于 DOM 元素和其它腳本的執(zhí)行結果時,我們會選用 defer。

DOM的優(yōu)化

  • 回流和重繪

回流:當我們對 DOM 的修改引發(fā)了 DOM 幾何尺寸的變化(比如修改元素的寬、高或隱藏元素等)時,瀏覽器需要重新計算元素的幾何屬性(其他元素的幾何屬性和位置也會因此受到影響),然后再將計算的結果繪制出來。這個過程就是回流(也叫重排)。
重繪:當我們對 DOM 的修改導致了樣式的變化、卻并未影響其幾何屬性(比如修改了顏色或背景色)時,瀏覽器不需重新計算元素的幾何屬性、直接為該元素繪制新的樣式(跳過了上圖所示的回流環(huán)節(jié))。這個過程叫做重繪。
由此我們可以看出,重繪不一定導致回流,回流一定會導致重繪。我們對dom的優(yōu)化主要在于減少DOM操作;

  • 回流的“導火索”

改變 DOM 元素的幾何屬性
改變 DOM 樹的結構
獲取一些特定屬性的值:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight...

  • 規(guī)避回流與重繪

js先用變量保存好要計算的值,最終再設置dom
避免逐條改變樣式,使用類名去合并樣式
將 DOM “離線”,先設置display:none;中間操作,后面再設置display:block;

  • 瀏覽器Flush 隊列
  • DOM Fragment 需要了解一下

本質上是作為脫離了真實 DOM 樹的容器出現(xiàn),用于緩存批量化的 DOM 操作

Event Loop 與異步更新策略

  • macro(洪任務): setTimeout、setInterval、 setImmediate、script(整體代碼)、 I/O 操作、UI 渲染等。
  • micro-task(微任務): process.nextTick、Promise、MutationObserver

當我們需要在異步任務中實現(xiàn) DOM 修改時,把它包裝成 micro 任務是相對明智的選擇。

  • Vue狀態(tài)更新手法:nextTick
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 檢查上一個異步任務隊列(即名為callbacks的任務數(shù)組)是否派發(fā)和執(zhí)行完畢了。pending此處相當于一個鎖
  if (!pending) {
    // 若上一個異步任務隊列已經執(zhí)行完畢,則將pending設定為true(把鎖鎖上)
    pending = true
    // 是否要求一定要派發(fā)為macro任務
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      // 如果不說明一定要macro 你們就全都是micro
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

實際上也是運用了promise

lazy-load(懶加載)

在懶加載的實現(xiàn)中,有兩個關鍵的數(shù)值:一個是當前可視區(qū)域的高度,另一個是元素距離可視區(qū)域頂部的高度。

事件的節(jié)流(throttle)與防抖(debounce)

像scroll,resize,keyup等事件頻繁觸發(fā)會引發(fā)頁面的抖動甚至卡頓
節(jié)流”與“防抖”是以閉包的形式來實現(xiàn)的;
它們通過對事件對應的回調函數(shù)進行包裹、以自由變量的形式緩存時間信息,最后用 setTimeout 來控制事件的觸發(fā)頻率。

  • 節(jié)流和防抖結合體
// fn是我們需要包裝的事件回調, delay是時間間隔的閾值
function throttle(fn, delay) {
  // last為上一次觸發(fā)回調的時間, timer是定時器
  let last = 0, timer = null
  // 將throttle處理結果當作函數(shù)返回
  
  return function () { 
    // 保留調用時的this上下文
    let context = this
    // 保留調用時傳入的參數(shù)
    let args = arguments
    // 記錄本次觸發(fā)回調的時間
    let now = +new Date()
    
    // 判斷上次觸發(fā)的時間和本次觸發(fā)的時間差是否小于時間間隔的閾值
    if (now - last < delay) {
    // 如果時間間隔小于我們設定的時間間隔閾值,則為本次觸發(fā)操作設立一個新的定時器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 如果時間間隔超出了我們設定的時間間隔閾值,那就不等了,無論如何要反饋給用戶一次響應
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的throttle包裝scroll的回調
document.addEventListener('scroll', throttle(() => console.log('觸發(fā)了滾動事件'), 1000))...

谷歌瀏覽器自帶的Performance以及瀏覽器插件LightHouse可以監(jiān)測網站性能;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容