webpack--前端性能優化與Gzip原理


目錄

前言

前不久看過掘金小冊《前端性能優化原理與實踐》,受益匪淺。“我深感性能優化實在是前端知識樹中特別的一環——當你需要學習前端框架時,文檔和源碼幾乎可以告訴你所有問題的答案,當你需要學習 Git 時,你也可以找到放之四海皆准的實踐方案。但性能優化卻不一樣,它好像只能是一個摸索的過程。” 前端的性能優化一直是困擾我的一個問題, 我不知道哪些問題需要優化,這些問題又應該用什么方式和方法優化?即使知道卻總感覺知之甚少,自己項目真正去做優化的卻不多。“任何技術的掌握,都離不開一定比例的理論基礎和實際操作的支撐。” 通過這個專題,記錄自己了解的前端性能優化以及在自己的項目中去做優化。

webpack 的性能瓶頸

  • webpack 的構建過程太花時間
  • webpack 打包的結果體積太大

webpack 優化方案

優化Loader

優化 Loader 的文件搜索范圍

module.exports = {
  module: {
    rules: [
      {
        // js 文件才使用 babel
        test: /\.js$/,
        loader: 'babel-loader',
        // 只在 src 文件夾下查找
        include: [resolve('src')],
        // 不會去查找的路徑
        exclude: /node_modules/
      }
    ]
  }
}

將 Babel 編譯過的文件緩存起來

loader: 'babel-loader?cacheDirectory=true'

DllPlugin 打包第三方庫

DllPlugin 是基於 Windows 動態鏈接庫(dll)的思想被創作出來的。這個插件會把第三方庫單獨打包到一個文件中,這個文件就是一個單純的依賴庫。這個依賴庫不會跟着你的業務代碼一起被重新打包,只有當依賴自身發生版本變化時才會重新打包。

用 DllPlugin 處理文件,要分兩步走:

  • 基於 dll 專屬的配置文件,打包 dll 庫
  • 基於 webpack.config.js 文件,打包業務代碼

以自己的vue項目為例,我的dll配置文件如下:

新建webpack.dll.config.js文件

const path = require('path')
const webpack = require('webpack')
const CleanWebpackPlugin = require('clean-webpack-plugin')

// dll文件存放的目錄
const dllPath = 'public/vendor'

module.exports = {
  entry: {
    // 需要提取的庫文件
    vendor: ['vue', 'vue-router', 'vuex', 'axios', 'element-ui']
  },
  output: {
    path: path.join(__dirname, dllPath),
    filename: '[name].dll.js',
    // vendor.dll.js中暴露出的全局變量名
    // 保持與 webpack.DllPlugin 中名稱一致
    library: '[name]_[hash]'
  },
  plugins: [
    // 清除之前的dll文件
    new CleanWebpackPlugin(['*.*'], {
      root: path.join(__dirname, dllPath)
    }),
    // 設置環境變量
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: 'production'
      }
    }),
    // manifest.json 描述動態鏈接庫包含了哪些內容
    new webpack.DllPlugin({
      path: path.join(__dirname, dllPath, '[name]-manifest.json'),
      // 保持與 output.library 中名稱一致
      name: '[name]_[hash]',
      context: process.cwd()
    })
  ]
}

在vue.config.js中配置dll

  plugins: [
    new webpack.DllReferencePlugin({
      context: process.cwd(),
      manifest: require('./public/vendor/vendor-manifest.json')
    }),
    // 將 dll 注入到 生成的 html 模板中
    new AddAssetHtmlPlugin({
      // dll文件位置
      filepath: path.resolve(__dirname, './public/vendor/*.js'),
      // dll 引用路徑
      publicPath: './vendor',
      // dll最終輸出的目錄
      outputPath: './vendor'
    }),
  ]

HappyPack 並行打包

受限於 Node 是單線程運行的,所以 Webpack 在打包的過程中也是單線程的,特別是在執行 Loader 的時候,長時間編譯的任務很多,這樣就會導致等待的情況。

HappyPack 可以將 Loader 的同步執行轉換為並行的,這樣就能充分利用系統資源來加快打包效率了

module: {
  loaders: [
    {
      test: /\.js$/,
      include: [resolve('src')],
      exclude: /node_modules/,
      // id 后面的內容對應下面
      loader: 'happypack/loader?id=happybabel'
    }
  ]
},
plugins: [
  new HappyPack({
    id: 'happybabel',
    loaders: ['babel-loader?cacheDirectory'],
    // 開啟 4 個線程
    threads: 4
  })
]

Tree Shaking 刪除冗余代碼

從 webpack2 開始,webpack 原生支持了 ES6 的模塊系統,並基於此推出了 Tree-Shaking。webpack 官方是這樣介紹它的:

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination, 
or more precisely, live-code import. It relies on ES2015 module import/export for the static 
structure of its module system.

意思是基於 import/export 語法,Tree-Shaking 可以在編譯的過程中獲悉哪些模塊並沒有真正被使用,這些沒用的代碼,在最后打包的時候會被去除。

在 Webpack3 中,我們一般使用 UglifyJS 來刪除壓縮代碼,我們可以使用 uglifyjs-webpack-plugin 來並行運行 UglifyJsPlugin,從而提高效率。

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
 plugins: [
   new UglifyJsPlugin({
     // 允許並發
     parallel: true,
     // 開啟緩存
     cache: true,
     compress: {
       // 刪除所有的console語句    
       drop_console: true,
       // 把使用多次的靜態值自動定義為變量
       reduce_vars: true,
     },
     output: {
       // 不保留注釋
       comment: false,
       // 使輸出的代碼盡可能緊湊
       beautify: false
     }
   })
 ]
}

手動引入 UglifyJsPlugin 的代碼其實是 webpack3 的用法,webpack4 現在已經默認使用 uglifyjs-webpack-plugin 對代碼做壓縮了——在 webpack4 中,我們是通過配置 optimization.minimize 與 optimization.minimizer 來自定義壓縮相關的操作的。

按需加載

思想:

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

已vue路由為例,把路由引入方式改為下面方式:

{
name: 'login',
path: '/login',
component: () => import('@/views/login/login.vue') // 按需加載
}

Gzip 壓縮原理

摘自《前端性能優化原理與實踐》

說到壓縮,可不只是構建工具的專利。我們日常開發中,其實還有一個便宜又好用的壓縮操作:開啟 Gzip。

具體的做法非常簡單,只需要你在你的 request headers 中加上這么一句:

accept-encoding:gzip

HTTP 壓縮:

HTTP 壓縮是一種內置到網頁服務器和網頁客戶端中以改進傳輸速度和帶寬利用率的方式。
在使用 HTTP 壓縮的情況下,HTTP 數據在從服務器發送前就已壓縮:兼容的瀏覽器將在下
載所需的格式前宣告支持何種方法給服務器;不支持壓縮方法的瀏覽器將下載未經壓縮的數據。
最常見的壓縮方案包括 Gzip 和 Deflate。

以上是摘自百科的解釋,事實上,大家可以這么理解:

HTTP 壓縮就是以縮小體積為目的,對 HTTP 內容進行重新編碼的過程

Gzip 的內核就是 Deflate,目前我們壓縮文件用得最多的就是 Gzip。可以說,Gzip 就是 HTTP 壓縮的經典例題。

該不該用 Gzip

如果你的項目不是極端迷你的超小型文件,我都建議你試試 Gzip。

有的同學或許存在這樣的疑問:壓縮 Gzip,服務端要花時間;解壓 Gzip,瀏覽器要花時間。中間節省出來的傳輸時間,真的那么可觀嗎?

答案是肯定的。如果你手上的項目是 1k、2k 的小文件,那確實有點高射炮打蚊子的意思,不值當。但更多的時候,我們處理的都是具備一定規模的項目文件。實踐證明,這種情況下壓縮和解壓帶來的時間開銷相對於傳輸過程中節省下的時間開銷來說,可以說是微不足道的。

Gzip 是萬能的嗎

首先要承認 Gzip 是高效的,壓縮后通常能幫我們減少響應 70% 左右的大小。

但它並非萬能。Gzip 並不保證針對每一個文件的壓縮都會使其變小。

Gzip 壓縮背后的原理,是在一個文本文件中找出一些重復出現的字符串、臨時替換它們,從而使整個文件變小。根據這個原理,文件中代碼的重復率越高,那么壓縮的效率就越高,使用 Gzip 的收益也就越大。反之亦然。

webpack 的 Gzip 和服務端的 Gzip

一般來說,Gzip 壓縮是服務器的活兒:服務器了解到我們這邊有一個 Gzip 壓縮的需求,它會啟動自己的 CPU 去為我們完成這個任務。而壓縮文件這個過程本身是需要耗費時間的,大家可以理解為我們以服務器壓縮的時間開銷和 CPU 開銷(以及瀏覽器解析壓縮文件的開銷)為代價,省下了一些傳輸過程中的時間開銷。

既然存在着這樣的交換,那么就要求我們學會權衡。服務器的 CPU 性能不是無限的,如果存在大量的壓縮需求,服務器也扛不住的。服務器一旦因此慢下來了,用戶還是要等。Webpack 中 Gzip 壓縮操作的存在,事實上就是為了在構建過程中去做一部分服務器的工作,為服務器分壓。

因此,這兩個地方的 Gzip 壓縮,誰也不能替代誰。它們必須和平共處,好好合作。作為開發者,我們也應該結合業務壓力的實際強度情況,去做好這其中的權衡。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM