幾種常見的基礎前端優化方式

這篇文章算是最近學習前端優化時的一點心得,爲了對比強烈下降了CPU性能,文中代碼在 github上也有一部分。
本文性能截圖來自chrome自帶的performance,不瞭解的能夠看看跟他差很少的前世chrome timeline(介紹 傳送門)。

基礎

CSS選擇符優化

衆所周知,css選擇符解析順序爲從右向左,因此#id div的解析速度就不如div #idcss

減小回流重繪

瀏覽器渲染大體流程是這樣的:html

  1. 處理HTML標記並構造DOM樹。
  2. 處理CSS標記並構造CSS規則樹。
  3. 將DOM樹與CSS規則樹合併成一個渲染樹。
  4. 根據渲染樹來佈局,以計算每一個節點的幾何信息。
  5. 將各個節點繪製到屏幕上。

當 Render Tree 中部分或所有, 因元素的尺寸、佈局、隱藏等改變而須要從新構建,瀏覽器從新渲染的過程稱爲迴流。
會致使迴流的操做:前端

  • 頁面首次渲染。
  • 瀏覽器窗口大小發生改變。
  • 元素尺寸或者位置發生改變。
  • 元素內容變化(文字數量或者圖片大小發生改變)。
  • 元素字體大小的改變。
  • 添加或者刪除可見的 DOM 元素。
  • 激活 CSS 僞類。
  • 查詢某些屬性或調用某些方法。

一些經常使用且會致使迴流的屬性和方法。vue

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

當頁面中元素樣式的改變並不影響佈局時(像colorbackground-color等),瀏覽器會將新樣式賦予給元素並從新繪製它,這個過程稱爲重繪。迴流必將引發重繪,重繪不必定會引發迴流。node

  • 緩存layout屬性
    瀏覽器會維護一個隊列,把全部引發迴流和重繪的操做放入隊列中,若是隊列中的任務數量或者時間間隔達到一個閾值的,瀏覽器就會將隊列清空,進行一次批處理,這樣能夠把屢次迴流和重繪變成一次。可是訪問前面所說的屬性和調用方法,瀏覽器會爲了準確性而清空隊列強制進行迴流,因此咱們能夠緩存layout屬性,來避免這種現象,好比寫滾動加載的時候,就能夠緩存offsetTop等屬性,避免每次對比都產生迴流。若是你沒有這樣作,可愛的瀏覽器甚至會提醒Forced reflow is a likely performance bottleneck.
  • 將屢次迴流的元素放在absolute中
    使用absolute,把會引發迴流的動畫脫離文檔流,它的變化就不會影響到其餘元素。須要注意的是,雖然float也是脫離文檔流,但其餘盒子內的文本依然會爲這個元素讓出位置,環繞在周圍。而對於使用absolute脫離文檔流的元素,其餘盒子與其餘盒子內的文本都會無視它。纔是真正的不會影響。(實際測試float甚至還不如relative)
  • 批量修改DOM
    好比一個列表,如今須要向裏面push100項新內容,一項一項添加的話,則至少會有100次迴流,若是使用DocumentFragment分10次處理,就只會有10次迴流。那是否是隻處理一次,就會有一次迴流,這樣性能更好呢,並非。舉個栗子,我想要吃100份炸雞,若是一份一份吃會很累,若是一次直接吃100份,會直接撐炸,比較好的方式就是分10次吃,每次吃10份。這其中涉及到的long task概念,也是下一個優化方式所涉及的。
任務切片

學名task-slice,算是一個必備的優化方式了,着重說一哈,先來看吃炸雞的例子,爲了突出優化先後差別把要吃的炸雞變成1000份。
實驗1:一份一份吃 吃1000次
image.png
實驗2:一次吃1000份
image.png
實驗3:分10次,每次吃100份
image.png
能夠看到黃條表明的scripting從一段變成了好幾段,對應的task也從一長條分了好幾份。前文中緩存layout屬性的部分講過,瀏覽器會維護一個隊列,因此實驗1和實驗2結果差距不大是由於他們都被放進隊列中最後統一處理,而task-slice作的就是把一個long task,分紅幾個小task交給瀏覽器依次處理,緩解瀏覽器短期內的壓力。幀數也從2提高到了10+。(由於我測試時閹割了性能,因此優化後幀數依然感人)react

上面這個例子,是同步的任務切片,那萬一可愛的項目經理說要加10個echarts圖咋辦嘞。
其實同步和異步差很少的,上一個簡單版本的代碼webpack

function TaskSlice(num, fn) {
  this.init(num, fn)
}
TaskSlice.prototype = {
  init: (num, fn) => {
    let index = 0
    function next() {
      if(index < num) {
        fn.call(this, index, next)
      }
      index++
    }
    next()
  }
}

使用的時候就這樣git

function drawCharts (num) {
  new TaskSlice(
    num,
    drawChart
  )
}

function drawChart(id, cb) {
  var chart = echarts.init(document.getElementById(id))
  chart.on('finished', cb)
  chart.on('finished', () => {
    chart.off()
  })
  chart.setOption(options)
}

由於echarts的生命週期是本身內部定義的事件,因此看起來比較麻煩,若是想要切片的異步任務是promise就比較簡單了github

function asyncTask(cb) {
  promise().then(() => {
    // balabalaba
    cb()
  })
}

這個類的邏輯大概是這樣的:
初始化時傳入要切片的次數num和異步的任務fn;
而後定義一個函數next,next經過閉包維護一個表示當前執行任務次數的變量index,而後調用next進入函數內邏輯;
判斷執行次數是否小於要切的次數,小於的話,調用fn,同時給他兩個參數分別爲當前執行次數和next
而後進入fn函數,這裏只須要在異步完成後調用next,任務就被切成了好多片。web

減小做用域查找

做用域鏈和原型鏈相似,當咱們使用對象的某一個屬性時,會遍歷原型鏈,從當前對象的屬性開始,若是找不到該屬性,則會在原型鏈上往下一個位置移動,尋找對應的屬性,直到找到屬性或者達到原型鏈末端。
在做用域鏈中,首先會在當前做用域中尋找咱們須要的變量或者函數,若是沒找到的話,則往上一個做用域尋找,直到找到變量/函數或者到達全局做用域。

//bad
var a=1;
function fn(){
  console.log(a);
}
fn()

//good
var a=1;
function fn(value){
  console.log(value);
}
fn(a)
節流防抖

throttle&debounce,這個網上文章太多了,並且像lodash這種工具庫也有現成的源碼,我也寫了一個簡版的,可能更通俗一點,就在文章開頭說的github裏,須要注意的是他們不能減小事件的觸發次數。學就完事兒了。

懶加載

先將img標籤中的src連接設爲同一張圖片,將其真正的圖片地址存儲在img標籤的自定義屬性。當js監聽到該圖片元素進入可視窗口時,再把src的值替換爲自定義屬性,減小首屏加載的請求數量,達到懶加載的效果。
其中的定義滾動事件,和計算是否進入可視窗口,就用到了前面說的防抖和緩存layout屬性

let pending = false

function LazyLoad({ els, lazyDistance }) {
  this.lazyDistance = lazyDistance
  this.imglist = Array.from(els)
  this.loadedLen = 0
  this.init()
}
LazyLoad.prototype = {
  init: function() {
    this.initHandler()
    this.lazyLoad()
  },

  load: function(el) {
    if(!el.loaded) {
      el.src = el.getAttribute('data-src')
      this.loadedLen++
      el.loaded = true
    }
  },

  lazyLoad: function() {
    for(let i = 0; i < this.imglist.length; i++) {
      this.getBound(this.imglist[i]) && this.load(this.imglist[i])
    }
    pending = false
  },

  getBound: function(el) {
    let bound = el.getBoundingClientRect()
    let clientHeight = document.documentElement.clientHeight || document.body.clientHeight
    return bound.top <= clientHeight + this.lazyDistance
  },

  initHandler: function() {
    const fn = throttle(function() {
      if(!pending) {
        pending = true
        if(this.imglist.length > this.loadedLen) {
          this.lazyLoad()
        } else {
          window.removeEventListener('scroll', this.scrollHander, false)
        }
      }
    }, 1000)
    this.scrollHander = fn.bind(this)

    window.addEventListener('scroll', this.scrollHander, false)
  },
}

Vue

函數式組件

能夠把沒有狀態,沒有this上下文,沒有生命週期的組件,寫爲函數式組件,由於函數式組件只是函數,因此渲染開銷也低不少。具體寫法官網傳送門

拆分子組件

由於vue的渲染順序爲先父到子,因此拆分子組件相似上面所說的task slice。就是把一個大的task分紅了父和子兩個task。

使用v-show複用dom

下面這段話抄自官網
v-if 是「真正」的條件渲染,由於它會確保在切換過程當中條件塊內的事件監聽器和子組件適當地被銷燬和重建。
v-show 就簡單得多——無論初始條件是什麼,元素老是會被渲染,而且只是簡單地基於 CSS 進行切換。

使用keep-alive進行緩存

keep-alive是Vue內置組件,會緩存該組件內的組件的實例,節省再次渲染時初始化組件的花銷。

延遲加載DOM

這一項其實仍是任務切片,可是這種實現方式真的和Vue特別契合,直接上代碼

export default function (count = 10) {
  return {
    data () {
      return {
        displayPriority: 0,
      }
    },

    mounted () {
      this.runDisplayPriority()
    },

    methods: {
      runDisplayPriority () {
        const step = () => {
          requestAnimationFrame(() => {
            this.displayPriority++
            if (this.displayPriority < count) {
              step()
            }
          })
        }
        step()
      },

      defer (priority) {
        return this.displayPriority >= priority
      },
    },
  }
}

函數返回一個mixin,經過defer函數和v-if來控制切片,像這樣:
image.png

不響應式數據
  • 衆所周知,new一個Vue時,Vue會遍歷data中的屬性經過Object.defineProperty(2.x版本)來將他們設置爲響應式數據,當其中的屬性變化時,經過觸發屬性的set去更新View。那麼若是隻是爲了定義一些常量,咱們就不須要vue去設置他們爲響應式,寫在created裏面就能夠了。
  • 一個table組件的props確定會有一個數組,常見的寫法像這樣

    <template>
      <el-table :data="list">
      <!--一些內容-->
      </el-table>
    </template>
    <script>
    // 一些內容
    data() {
      return {
        list: []
      }
    }
    created() {
      this.fetch() // 獲取數據賦值list
    }
    </script>

    我一開始也以爲這種寫法無比正常,list須要是響應式的,由於須要table隨着list的改變而改變,更況且element-ui官網的示例就是將list的聲明放在data中。然鵝,真正起做用的是做爲props傳進table組件的list,而不是再父組件中的list。因此這個list的聲明也是沒有必要放在data裏的。

  • 仍是以上面的table組件爲例,由於vue會遞歸遍歷data和props的全部屬性,因此當list傳進時,假設list的結構是這樣的[{id: 1, name: '前端'}],那麼id和name兩個屬性也會被設置爲響應式,若是需求這兩個屬性只須要展現,那麼能夠這樣作

    function optimizeItem (item) {
      const data = {}
      Object.defineProperty(data, 'data', {
        configurable: false,
        value: item,
      })
      return data
    }

    經過設置屬性的configurable爲false來阻止vue再去修改他。

webpack

縮小文件搜索範圍
  • 優化loader配置
    使用loader時能夠經過testincludeexclude來命中匹配的文件,讓儘量少的文件被處理。
  • resolve.alias
    resolve.alias 經過別名來把原導入路徑映射成一個新的導入路徑,在項目種常常會依賴一些龐大的第三方模塊,以react爲例,默認狀況下 Webpack 會根據庫的package.json中定義的入口文件 ./node_modules/react/react.js 開始遞歸的解析和處理依賴的幾十個文件,這會時一個耗時的操做。 經過配置 resolve.alias 可讓 Webpack 在處理 React 庫時,直接使用單獨完整的 react.min.js 文件,從而跳過耗時的遞歸解析操做。(vue系的庫的入口文件就直接是一個單獨的完整的文件,牛批)
    通常對於總體性強的庫可使用這種方法,可是像loadsh這種,可能只使用了其中幾個函數,若是也這樣設置,就會致使輸出文件中有不少廢代碼。

    resolve: {
      alias: {
        'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js')
      }
    },
  • resolve.extensions
    在導入語句沒帶文件後綴時,Webpack 會根據resolve.extensions的配置帶上後綴後去嘗試詢問文件是否存在。默認值是['.wasm', '.mjs', '.js', '.json'](v4.41.2)。也就是說當遇到 require('./data') 這樣的導入語句時,Webpack 會先去尋找 ./data.wasm 文件,若是該文件不存在就去尋找 ./data.mjs 文件,以此類推,最後若是找不到就報錯。
    若是這個列表越長,或者正確的後綴在越後面,就會形成嘗試的次數越多,因此 resolve.extensions 的配置也會影響到構建的性能。 在配置 resolve.extensions 時你須要遵照如下幾點,以作到儘量的優化構建性能:
    一、後綴嘗試列表要儘量的小,不要把項目中不可能存在的狀況寫到後綴嘗試列表。
    二、頻率出現高的文件後綴要優先放在前面。
    三、在源碼中寫導入語句時,要儘量的帶上後綴,從而能夠避免尋找過程。例如在你肯定的狀況下把 require('./data') 寫成 require('./data.json')。

    resolve: {
      extensions: ['.js', '.vue'],
    },
  • module.noParse
    這個配置項可讓webpack對沒有采用模塊化的文件不進行處理,被忽略的文件不該該具備import、require等導入機制的調用。像上面resolve.alias中的單獨的完整的react.min.js就沒有采用模塊化。忽略以後能夠提升構建性能。

    module: {
      noParse: [/vue\.runtime\.common\.js$/],
    },
壓縮代碼

瀏覽器從服務器訪問網頁時獲取的 JavaScript、CSS 資源都是文本形式的,文件越大網頁加載時間越長。 爲了提高網頁加速速度和減小網絡傳輸流量,能夠對這些資源進行壓縮。js可使用webpack內置的uglifyjs-webpack-plugin插件,css可使用optimize-css-assets-webpack-plugin

optimization: {
  minimizer: [
    new UglifyJsPlugin(),
    new OptimizeCSSAssetsPlugin()
  ]
}
DllPlugin

dll是動態連接庫,在一個動態連接庫中能夠包含給其餘模塊調用的函數和數據。包含基礎的第三方模塊(如vue全家桶)的動態連接庫只須要編譯一次,以後的構建中這些模塊就不須要從新編譯,而是直接使用動態連接庫中的代碼。因此會大大提高構建速度。
具體操做是使用DllPluginDllReferencePlugin這兩個內置的插件,前者用於打包出動態連接庫文件,後者用於主webpack配置中去引用。

// 打包dll
entry: {
  vendor: ['vue', 'vue-router', 'vuex'],
},
output: {
  filename: '[name].dll.js',
  path: path.resolve(__dirname, 'dist'),
  library: '_dll_[name]',
},
plugins: [
  new DllPlugin({
    name: '_dll_[name]',
    path: path.join(__dirname, 'dist', '[name].manifest.json'),
  }),
],
// output和plugins中的[name]都是entry中的key,
// 也就是'vender'
// 引用
plugins: [
  new DllReferencePlugin({
    manifest: require('../dist/vendor.manifest.json'),
  }),
]
happypack

因爲運行在Node.js之上的Webpack是單線程的,因此Webpack須要處理的任務會一件件挨着作,不能多個事情一塊兒作。而HappyPack能夠把任務分解給多個子進程去併發的執行,子進程處理完後再把結果發送給主進程。很少bb上代碼

const HappyPack = require('happypack')

module: {
  rules: [
    {
      test: /\.js$/,
      use: ['happypack/loader?id=babel']
    }
  ],
},
plugins: [
  new HappyPack({
    // 用惟一的標識符 id 來表明當前
    // 的 HappyPack 是用來處理一類特定的文件
    id: 'bable',
    loaders: ['babel-loader'],
  })
]

可是HappyPack(v5.0.1)並不支持vue-loader(v15.3.0)(支持列表),而在vue的項目中,使用模板語法的話大部分的業務js都是寫在.vue文件中的,就能夠經過配置vue-loader的options部分,將js部分交由happypack處理

好像以前的vue-loader是支持的,改爲須要在pulgins裏面單獨聲明以後就不行了,而vue-loader升級是加快了打包速度的,強行爲了使用happypack而降級有點捨本逐末的味道。
//rules: [
//  {
//    test: /\.vue$/,
//    use: [
//      {
//        loader: 'vue-loader',
//        options: {
//          loaders: {
//            js: 'happypack/loader?id=babel'
//          },
//        }
//      }
//    ]
//  }
//]

不支持也沒有關係,vue Loader文檔有說,在pulgins中引用能夠將你定義過的其它規則複製並應用到.vue文件裏相應語言的塊。例如,若是你有一條匹配/\.js$/的規則,那麼它會應用到.vue文件裏的<script>塊。

其餘

WebAssembly

瞭解這個東西是看webpack文檔的時候,發現resolve.extensions的默認配置是['.wasm', '.mjs', '.js', '.json'],這個wasm甚至是排在第一位的,就去了解了一下,真是不看不知道一看嚇一跳,這玩意兒也忒厲害咧,個人理解瀏覽器識別js代碼的大概流程是下載->轉換->編譯,可是wasm能夠跳過轉換和編譯兩步,由於他自己就能夠被瀏覽器識別,從而並且最近WebAssembly也正式加入到W3C標準了,別問,問就是知識點。放一個[mdn對於WebAssembly的介紹]看成拓展閱讀(https://developer.mozilla.org...

相關文章
相關標籤/搜索