首屏加載優化的webpack實踐

profile 概覽

本文總結了一些首屏加載優化的webpack實踐, 分爲以下幾個模塊

  • bundle analysis 打包分析
  • code coverage 代碼覆蓋率
  • magic comments 魔法註釋
  • prefeching/preloading 預加載
  • code splitting 代碼分割
  • lazy loading 懶加載(按需加載)
  • babel, babel-polyfill與babel-runtime
  • tree shaking 搖樹
  • sourcemap 源碼映射配置

本文所用demo代碼的倉庫地址

github.com/atbulbs/web…javascript

bundle analysis 打包分析

官方推薦的一些打包分析插件 webpack.js.org/guides/code…css

安裝和使用webpack-bundle-analyzer github.com/webpack-con…html

# 生成stats.json文件
$ webpack --profile --json > stats.json
複製代碼

code coverage 代碼覆蓋率

官方文檔: developers.google.com/web/updates…前端

參考文檔: blog.logrocket.com/using-the-c…java

  • 適用於JS和CSS
  • 若某文件代碼覆蓋率低, 影響首屏時間和性能
  • 解決: 首屏用戶交互後才執行的關鍵代碼預加載 + 非關鍵代碼懶加載 + 移除無用代碼 tree shaking
// 查看代碼覆蓋率
// dev tool > command + shift + P > Show Coverage > 錄屏
複製代碼

magic comments 魔法註釋

官方文檔: webpack.js.org/api/module-…node

function getComponent () {
  return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['a', 'b'], '*')
    return element
  })
}
複製代碼

prefeching/preloading 預加載

官方文檔: webpack.js.org/guides/code…webpack

實現第一次加載的時候就是最快的, webpack推薦交互的代碼放到異步加載的模塊裏去寫 prefeching/preloading可實現網頁空閒時預先加載異步模塊git

// src/click.js
export default funciton clickHandler () {
  console.log('clicked')
}

// src/index.js
window.document.addEventListener('click', () => {
  // prefetch會等待覈心代碼加載完成, 頁面空閒時去加載prefetch的文件
  // webpackPreload會和核心代碼一塊兒加載
  import(/* webpackPrefetch: true */'./click.js').then({ default: func } => func())
})

複製代碼

code splitting 代碼分割

官方文檔 webpack.js.org/guides/code…es6

  • 分割業務代碼和庫代碼, 否則打包文件會很大, 首次訪問加載時間會很長github

  • 並且若是不分割, 修改業務代碼後, 從新訪問, 又所有得從新加載庫代碼

  • 分割方式: 配置 + 同步引入 與 動態引入(無需作任何配置)

    動態引入文檔 webpack.js.org/guides/code…

function getComponent () {
  // jsonp引入
  // 動態的import, 實驗性的語法
  // npm i babel-plugin-dynamic-import-webpack -D
  return import('lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['a', 'b'], '*')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})

// .babelrc 動態引入
// npm i -D babel-plugin-dynamic-import-webpack
{
  plugins: ['dynamic-import-webpack']
}
複製代碼

SplitChunksPlugin官方文檔: webpack.js.org/plugins/spl…

// splitPlugin 配置
optimization: {
    // SplitChunksPlugin config
    // 以下是官方默認配置
    splitChunks: {
      // async 只對異步代碼生效
      // all 對同步異步都作代碼分割, 可是同步代碼還需cacheGrops配置
      // initial 只對同步代碼作分割
      chunks: 'async',
      // 若是引入的模塊大於minSize才作代碼分割
      minSize: 30000,
      // 對於大於maxsize的模塊嘗試進行二次代碼分割
      maxSize: 0,
      // 打包後的文件至少有多少個chunk文件引入這個模塊才進行代碼分割
      minChunks: 1,
      // 同時加載的模塊數量, 
      // 在打包前5個庫的時候會生成5個js文件,
      // 超過5個就再也不作代碼分割
      maxAsyncRequests: 5,
      // 入口文件作代碼分割的最大文件數量
      maxInitialRequests: 3,
      // 自動命名定界符
      automaticNameDelimiter: '~',
      // 讓cacheGroups裏的filename生效
      name: true,
      // 緩存組, 把庫文件先放到緩存裏, 再根據test規則分組合並打包
      cacheGroups: {
        // vendors: false
        vendors: {
          // 若是是node_modules裏面的文件, 就打包到vendors組裏
          test: /[\\/]node_modules[\\/]/,
          // 分組時的優先級
          priority: -10
          // 組文件的名字 vendors.js, 否則會是 vendors~main.js
          // filename: 'vendors.js' 
        },
        // 被分割的代碼的默認的配置, 沒有test, 全部模塊都符合要求
        default: {
          // 至少被引用了2次
          minChunks: 2,
          priority: -20,
          // 複用已被分割打包過了的模塊
          reuseExistingChunk: true,
          // 組的文件名
          // filename: 'common.js',
        }
      }
    }
  }
複製代碼

lazy loading 懶加載(按需加載)

官方文檔: webpack.js.org/guides/lazy…

前端框架結合webpack實現懶加載: webpack.js.org/guides/lazy…

// 點擊頁面纔會加載lodash代碼
function getComponent () {
  // 懶加載並非webpack裏面的一個概念, 而是ES的import語法, 
  // webpack能識別這種語法, 對import引入的模塊作代碼分割
  // 至關於對optimization的splitChunks作了配置
  return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['a', 'b'], '*')
    return element
  })
}

window.document.addEventListener('click', () => {
  getComponent().then(el => window.document.body.appendChild(el))
})
複製代碼

babel, babel-polyfill與babel-runtime

對比:

  • babel只轉換語法, 不轉換兼容api, 不少模塊中極可能有重複helper函數
  • babel-polyfill在全局作兼容, 會污染全局變量, 不建議在類庫中使用
  • babel-runtime屬於babel的包, 也可提供polyfill, 且是默認按需加載的, helper函數會做爲公共的模塊使用, 缺點: 業務代碼中的實例方法會失效
  • 使用了babel-plugin-transform-runtime, babel就會啓用bable-runtime

總結:

  • 業務代碼使用 babel-polyfill + 按需引入
  • 類庫代碼使用 babel-runtime

最佳實踐

  • 業務代碼可以使用 useBuiltIns 配置, 會自動按需引入babel-polyfill, 無需手動引入babel-polyfill

babeljs.io/docs/en/bab…

tree shaking 根據引入的按需打包, 搖晃掉模塊裏與樹沒有關聯的無用的模塊

官方文檔: webpack.js.org/guides/tree…

官方文檔: webpack.js.org/configurati…

關於靜態模塊結構的官方文檔: exploringjs.com/es6/ch_modu…

// Tree Shaking只支持 ES Module(靜態引入), 不支持Common JS(動態引入)
// development mode 默認沒有tree shaking
// production mode 不須要這個optimization
optimization: {
  usedExports: true
}

// package.json
"sideEffects": false, // false時對全部模塊搖樹
// 實踐
"sideEffects": [
  // 否則打包時會忽略 @babel/polly-fill, 由於其沒有導出對象, 只在window上掛載了對象
  "@babel/polly-fill",
  // 對css不搖樹
  "*.css"
], 
複製代碼

sourcemap 源碼映射

官方文檔: webpack.js.org/configurati…

devtool: 'none' // 關閉sourcemap
devtool: 'source-map' // 會生成一個.map文件
devtool: 'inline-source-map' // .map文件會被打包到js文件裏, 錯誤提示會精確到第幾行第幾列
devtool: 'cheap-inline-source-map' // 只精確到行, 不精確到列, 提示性能, 並且只會提示業務代碼的錯誤, 不提示loader和第三方模塊的錯誤
devtool: 'cheap-module-inline-source-map' // 提示loader和第三方模塊的錯誤
devtool: 'eval' // 用js eval效率最高, 提示不全面, 不展現行數
devtool: 'cheap-module-eval-source-map' // 開發時的最佳實踐
devtool: 'cheap-module-source-map' // 生產時的最佳實踐
複製代碼
相關文章
相關標籤/搜索