1. 程式人生 > 其它 >前端效能優化原理與實踐二

前端效能優化原理與實踐二

webpack 效能調優與 Gzip 原理

大家可以從第一節的示意圖中看出,我們從輸入 URL 到顯示頁面這個過程中,涉及到網路層面的,有三個主要過程:

  • DNS 解析
  • TCP 連線
  • HTTP 請求/響應

HTTP 連線這一層面的優化是我們網路優化的核心。因此我們開門見山,抓主要矛盾,直接從 HTTP 開始講起。

HTTP 優化有兩個大的方向:

  • 減少請求次數
  • 減少單次請求所花費的時間

這兩個優化點直直地指向了我們日常開發中非常常見的操作——資源的壓縮與合併。沒錯,這就是我們每天用構建工具在做的事情。而時下最主流的構建工具無疑是 webpack,所以我們這節的主要任務就是圍繞業界霸主 webpack 來做文章。

webpack 優化方案

構建過程提速策略

babel-loader 無疑是強大的,但它也是慢的。
最常見的優化方式是,用 include 或 exclude 來幫我們避免不必要的轉譯,比如 webpack 官方在介紹 babel-loader 時給出的示例:

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

除此之外,如果我們選擇開啟快取將轉譯結果快取至檔案系統,則至少可以將 babel-loader 的工作效率提升兩倍。要做到這點,我們只需要為 loader 增加相應的引數設定:

loader: 'babel-loader?cacheDirectory=true'
不要放過第三方庫

出於對效率的考慮,我們這裡為大家推薦 DllPlugin。DllPlugin 是基於 Windows 動態連結庫(dll)的思想被創作出來的。這個外掛會把第三方庫單獨打包到一個檔案中,這個檔案就是一個單純的依賴庫。這個依賴庫不會跟著你的業務程式碼一起被重新打包,只有當依賴自身發生版本變化時才會重新打包。

用 DllPlugin 處理檔案,要分兩步走:
  • 基於 dll 專屬的配置檔案,打包 dll 庫
  • 基於 webpack.config.js 檔案,打包業務程式碼
    以一個基於 React 的簡單專案為例,我們的 dll 的配置檔案可以編寫如下:
const path = require('path')
const webpack = require('webpack')

module.exports = {
    entry: {
      // 依賴的庫陣列
      vendor: [
        'prop-types',
        'babel-polyfill',
        'react',
        'react-dom',
        'react-router-dom',
      ]
    },
    output: {
      path: path.join(__dirname, 'dist'),
      filename: '[name].js',
      library: '[name]_[hash]',
    },
    plugins: [
      new webpack.DllPlugin({
        // DllPlugin的name屬性需要和libary保持一致
        name: '[name]_[hash]',
        path: path.join(__dirname, 'dist', '[name]-manifest.json'),
        // context需要和webpack.config.js保持一致
        context: __dirname,
      }),
    ],
}

編寫完成之後,執行這個配置檔案,我們的 dist 資料夾裡會出現這樣兩個檔案:

vendor-manifest.json
vendor.js

vendor.js 不必解釋,是我們第三方庫打包的結果。這個多出來的 vendor-manifest.json,則用於描述每個第三方庫對應的具體路徑,我這裡擷取一部分給大家看下:

{
  "name": "vendor_397f9e25e49947b8675d",
  "content": {
    "./node_modules/core-js/modules/_export.js": {
      "id": 0,
        "buildMeta": {
        "providedExports": true
      }
    },
    "./node_modules/prop-types/index.js": {
      "id": 1,
        "buildMeta": {
        "providedExports": true
      }
    },
    ...
  }
}  

隨後,我們只需在 webpack.config.js 裡針對 dll 稍作配置:

const path = require('path');
const webpack = require('webpack')
module.exports = {
  mode: 'production',
  // 編譯入口
  entry: {
    main: './src/index.js'
  },
  // 目標檔案
  output: {
    path: path.join(__dirname, 'dist/'),
    filename: '[name].js'
  },
  // dll相關配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // manifest就是我們第一步中打包出來的json檔案
      manifest: require('./dist/vendor-manifest.json'),
    })
  ]
}
Happypack——將 loader 由單程序轉為多程序

大家知道,webpack 是單執行緒的,就算此刻存在多個任務,你也只能排隊一個接一個地等待處理。這是 webpack 的缺點,好在我們的 CPU 是多核的,Happypack 會充分釋放 CPU 在多核併發方面的優勢,幫我們把任務分解給多個子程序去併發執行,大大提升打包效率。

HappyPack 的使用方法也非常簡單,只需要我們把對 loader 的配置轉移到 HappyPack 中去就好,我們可以手動告訴 HappyPack 我們需要多少個併發的程序:

const HappyPack = require('happypack')
// 手動建立程序池
const happyThreadPool =  HappyPack.ThreadPool({ size: os.cpus().length })

module.exports = {
  module: {
    rules: [
      ...
      {
        test: /\.js$/,
        // 問號後面的查詢引數指定了處理這類檔案的HappyPack例項的名字
        loader: 'happypack/loader?id=happyBabel',
        ...
      },
    ],
  },
  plugins: [
    ...
    new HappyPack({
      // 這個HappyPack的“名字”就叫做happyBabel,和樓上的查詢引數遙相呼應
      id: 'happyBabel',
      // 指定程序池
      threadPool: happyThreadPool,
      loaders: ['babel-loader?cacheDirectory']
    })
  ],
}
構建結果體積壓縮

這裡為大家介紹一個非常好用的包組成視覺化工具——webpack-bundle-analyzer,配置方法和普通的 plugin 無異,它會以矩形樹圖的形式將包內各個模組的大小和依賴關係呈現出來,格局如官方所提供這張圖所示:

在使用時,我們只需要將其以外掛的形式引入:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

按需載入

按需載入的思想:

  • 一次不載入完所有的檔案內容,只加載此刻需要用到的那部分(會提前做拆分)
  • 當需要更多內容時,再對用到的內容進行即時載入

當我們不需要按需載入的時候,我們的程式碼是這樣的:

import BugComponent from '../pages/BugComponent'
...
<Route path="/bug" component={BugComponent}>

為了開啟按需載入,我們要稍作改動。

首先 webpack 的配置檔案要走起來:

output: {
    path: path.join(__dirname, '/../dist'),
    filename: 'app.js',
    publicPath: defaultSettings.publicPath,
    // 指定 chunkFilename
    chunkFilename: '[name].[chunkhash:5].chunk.js',
},

路由處的程式碼也要做一下配合:

const getComponent => (location, cb) {
  require.ensure([], (require) => {
    cb(null, require('../pages/BugComponent').default)
  }, 'bug')
},
...
<Route path="/bug" getComponent={getComponent}>

小結

說了這麼多,我們都在討論檔案——準確地說,是文字檔案及其構建過程的優化。