一文搞定webpack構建優化策略

在以前的文章中,咱們瞭解了webpack的基本使用和原理:css

而當項目愈來愈複雜時,會面臨着構建速度慢和構建出來的文件體積大的問題。webapck構建優化對於大項目是必需要考慮的一件事,下面咱們就從速度和體積兩方面來探討構建優化的策略。html

分析工具

在優化以前,咱們須要瞭解一些量化分析的工具,使用它們來幫助咱們分析須要優化的點。vue

webpackbar

webpackbar能夠在打包時實時顯示打包進度。配置也很簡單,在plugins數組中加入便可:node

const WebpackBar = require('webpackbar')
module.exports = {
  plugins: [
    ...
    new WebpackBar()
  ]
}
複製代碼

image.png

speed-measure-webpack-plugin

使用speed-measure-webpack-plugin能夠看到每一個loader和plugin的耗時狀況。webpack

和普通插件的使用略有不一樣,須要用它的wrap方法包裹整個webpack配置項。git

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
module.exports = smp.wrap({
  entry: './src/main.js',
  ...
})
複製代碼

打包後,在命令行的輸出信息以下,咱們能夠看出哪些loader和plugin耗時比較久,而後對其進行優化。github

image,.png

webpack-bundle-analyzer

webpack-bundle-analyzer以可視化的方式讓咱們直觀地看到打包的bundle中到底包含哪些模塊內容,以及每個模塊的體積大小。咱們能夠根據這些信息去分析項目結構,調整打包配置,進行優化。web

在plugins數組中加入該插件。構建完成後,默認會在http://127.0.0.1:8888/展現分析結果。ajax

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

image.png webpack-bundle-analyzer會計算出模塊文件在三種情形下的大小:vue-router

  • stat:文件未通過任何轉換的原始大小
  • parsed:文件通過轉換後的輸出大小(好比babel-loader轉換ES6->ES五、UglifyJsPlugin壓縮等等)
  • gzip:parsed後的文件,通過Gzip壓縮的大小

使用speed-measure-webpack-pluginwebpack-bundle-analyzer自己也會增長打包時間(webpack-bundle-analyzer特別耗時),因此建議這兩個插件在開發分析時使用,而在生產環境去掉。

image.png

優化構建速度

多進程構建

運行在Node.js之上的 Webpack 是單線程的,就算有多個任務同時存在,它們也只能一個一個排隊執行。當項目比較複雜時,構建就會比較慢。現在大多數CPU都是多核的,咱們能夠藉助一些工具,充分釋放 CPU 在多核併發方面的優點。

比較常見的有happypackthread-loader

happypack

happypack可以將構建任務分解給多個子進程去併發執行,子進程處理完後再把結果發送給主進程。使用配置以下,就是把原有的loader的配置轉移到happyPack中去處理。

const Happypack = require('happypack')
module.exports = {
  module:{
    rules:[
      {
        test: /\.js$/,
        use: 'happypack/loader?id=babel'   //問號後面的查詢參數指定了處理這類文件的HappyPack實例的名字
      },
    ]
  },
  plugins:[
    new Happypack({
      id: 'babel',     //HappyPack實例名,對應上面rules中的「id=babel」
      use: ['babel-loader']     //本來要使用的loader
    })
  ]
}
複製代碼

thread-loader

happypack的做者已經沒有這個項目進行維護了,在webpack4以後,可使用thread-loader

thread-loader使用起來很簡單,就是把它放置在其它loader以前,以下所示。放置在這個thread-loader以後的 loaders會運行在一個單獨的worker池中。

module.exports = {
  module:{
    rules:[
      {
          test: /\.js$/,
          use: ['thread-loader','babel-loader']
      }
    ]
  },
}
複製代碼

image.png 若是是小項目,不建議開啓多進程構建,由於開啓進程是須要花費時間的,構建速度反而會變慢。

利用緩存

利用緩存能夠提高二次構建速度(下面的對比圖都是二次構建的速度)。使用緩存後,在node_modules中會有一個.cache目錄,用於存放緩存的內容。

cache-loader

在一些性能開銷較大的 loader 以前添加此cache-loader,以將結果緩存到磁盤中。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['cache-loader','babel-loader']
      }
    ]
  }
}
複製代碼

能夠看出,使用cache-loader後,構建速度有很是明顯的提高。

image.pngbabel-loader使用緩存,也能夠不借助cache-loader,直接在babel-loader後面加上?cacheDirectory=true

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader?cacheDirectory=true']
      }
    ]
  }
}
複製代碼

hard-source-webpack-plugin

hard-source-webpack-plugin用於開啓模塊的緩存。

const HardSourceWebpackPlugin = require("hard-source-webpack-plugin")
module.exports = {
  plugins:[
    new HardSourceWebpackPlugin()
  ]
}
複製代碼

使用hard-source-webpack-plugin後,二次構建速度大概提高了90%。

image.png

include/exclude

一般來講,loader會處理符合匹配規則的全部文件。好比babel-loader,會遍歷項目中用到的全部js文件,對每一個文件的代碼進行編譯轉換。而node_modules裏的js文件基本上都是轉譯好了的,不須要再次處理,因此咱們用 include/exclude 來幫咱們避免這種沒必要要的轉譯。

module.exports = {
  module:{
    rules:[
      {
          test: /\.js$/,
          use: ['babel-loader'],
          exclude: /node_modules/
          //或者  include: [path.resolve(__dirname, 'src')]
      }
    ]
  },
}
複製代碼

include直接指定查找文件夾,比exclude效率更高,更能提高構建速度。 image.png

動態連接庫

上面的babel-loader能夠經過include/exclude,避免處理node_modules裏的第三方庫。

但若是將第三方庫代碼和業務代碼都打包進一個bundle文件,那麼處理這個bundle文件的插件,好比uglifyjs-webpack-plugin、terser-webpack-plugin等,就沒辦法不處理裏面第三方庫內容。

其實第三方庫代碼基本都是成熟的,不用做什麼處理。所以,咱們能夠將項目的第三方庫代碼分離出來。

常見的處理方式有三種:

  1. Externals
  2. SplitChunks
  3. DllPlugin

Externals能夠避免處理第三方庫,但每個第三方庫都得在html文檔中增長一個script標籤來引入,一個頁面過多的js文件下載會影響網頁性能,並且有時咱們只使用第三方庫中的一小部分功能,用script標籤全量引入不太合理。

SplitChunks在每一次構建時都會從新構建第三方庫,不能有效提高構建速度。

這裏推薦使用DllPlugin和DLLReferencePlugin(配合使用),它們是webpack的內置插件。DllPlugin會將不頻繁更新的第三方庫單獨打包,當這些第三方庫版本沒有變化時,就不須要從新構建。

使用方法:

  1. 使用DllPlugin打包第三方庫
  2. 使用DLLReferencePlugin引用manifest.json,去關聯第1步中已經打好的包
  • 首先,新建一個webpack配置文件webpack.dll.js用於打包第三方庫(第1步)
const path = require('path')
const webpack = require('webpack')

module.exports = {
  mode: 'production',
  entry: {
    three: ['three', 'dat.gui']   // 第三方庫數組
  },
  output: {
    filename: '[name].dll.js',    //[name]就是在entry
    path: path.resolve(__dirname, 'dist/lib'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, 'dist/lib/[name].json') //manifest.json的存放位置
    })
  ]
}
複製代碼

打包好後,能夠看到,在dist目錄下增長了一個lib文件夾。 image.png

  • 而後,咱們在webpack.base.js作一下修改,去關聯第1步中已經打好的包(第2步)
module.exports = {
  plugins:[
    //修改CleanWebpackPlugin配置
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [  
        '!lib/**'              //在每次清楚dist目錄時,不清理lib文件夾的內容
      ]
    }),
    // dll相關配置
    new webpack.DllReferencePlugin({    
      // 將manifest字段配置成咱們第1步中打包出來的json文件
      manifest: require('./dist/lib/three.json')  
    })
  ]
}
複製代碼

再次打包後能夠看到,相比於一開始整個項目的體積 9.11MB,體積減少了90%,由於這是一個多頁面打包(多頁面打包配置)的應用,每一個頁面都引用了體積龐大的three.js核心文件,咱們把體積最大的three.js核心文件從每一個頁面的bundle中抽離出來後,bundle的體積大大減少。

image.png

再來看看構建時間:相比於使用DllPlugin以前,時間減小了30% 。 image.png

不只僅是第三方庫,業務代碼中的基礎庫也能夠經過進行DllPlugin分離。

優化構建體積

代碼分割

分離第三方庫和業務代碼中的基礎庫,能夠避免單個bundle.js體積過大,加載時間過長。而且在多頁面構建中,還能減小重複打包。

常見的操做是經過SplitChunks(以前的文章已經詳細地寫過了:SplitChunks)和 動態連接庫(如上所示),這裏都再也不贅述。

動態import

動態import的做用主要減小首屏資源的體積,非首屏的資源在用到的時候再去請求,從而提升首屏的加載速度。一個常見的例子就是單頁面應用的路由管理(好比vue-router

{
  path: '/list',
  name: 'List',
  component: () => import('../views/List.vue')  
},
複製代碼

不是直接import組件(import List from '../views/List.vue'),那樣會把組件都打包進同一個bundle。而是動態import組件,凡是經過import()引用的模塊都會打包到獨立的bundle,使用到的時候再去加載。對於功能複雜,又不是首屏必須的資源都推薦使用動態import。

<span @click="loadModal">show彈窗</span>
/***
methods: {
  loadModal(){
     import('../modal/index.js')
  }
}
***/
複製代碼

treeShaking

使用ES6的import/export語法,而且使用下面的方式導入導出你的代碼,而不要使用export default。

// util.js 導出
export const a = 1
export const b = 2
export function afunc(){}
或
export { a, b, afunc }

// index.js 導入
import { a, b } from './util.js'
console.log(a,b)
複製代碼

那麼在mode:production生產環境,就會自動開啓tree-shaking,移除沒有使用到的代碼,上面例子中的afunc函數就不會被打包到bundle中。

代碼壓縮

經常使用的js代碼壓縮插件有:uglifyjs-webpack-pluginterser-webpack-plugin

在webpack4中,生產環境默認開啓代碼壓縮。咱們也能夠本身配置去覆蓋默認配置,來完成更定製化的需求。

v4.26.0版本以前,webpack內置的壓縮插件是uglifyjs-webpack-plugin,從v4.26.0版本開始,換成了terser-webpack-plugin。咱們這裏也以terser-webpack-plugin爲例,和普通插件使用不一樣,在optimization.minimizer中配置壓縮插件

const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  optimization: {
    minimizer: [  
      new TerserPlugin({
        parallel: true,  //開啓並行壓縮,能夠加快構建速度
        sourceMap: true, //若是生產環境使用source-maps,則必須設置爲true
      })
    ]
  }
}
複製代碼

image.png

雪碧圖

雪碧圖將多張小圖標拼接成一張大圖,在HTTP1.x環境下,雪碧圖能夠減小HTTP請求,加速網頁的顯示速度。

用於合成雪碧圖的圖標體積要小,較大的圖片不建議拼接成雪碧圖;同時要是網站靜態圖標,不是經過ajax請求動態獲取的圖標。因此一般是做爲網站logo、icon之類的圖片。

開發時,能夠是UI提供雪碧圖,可是每新增一個圖標,就要從新制做一次,從新計算偏移量,比較麻煩。經過webpack插件合成雪碧圖,就能夠在開發時直接使用單個小圖標,在打包時,自動合成雪碧圖,並自動自動修改css中的background-position的值。

下面,咱們藉助postcss-sprites來自動合成雪碧圖。

首先,在webpack.base.js中配置postcss-loader

//webpack.base.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['vue-style-loader','css-loader', 'postcss-loader']  //配置postcss-loader
      },
      {
        test: /\.less$/,
        use: [
          'vue-style-loader','css-loader', 'postcss-loader', 'less-loader']  //配置postcss-loader
      }
    ]
  }
};
複製代碼

而後在項目根目錄下新建.postcssrc.js,配置postcss-sprites

module.exports = {
  "plugins": [
    require('postcss-sprites')({
      // 默認會合並css中用到的全部靜態圖片
      // 使用filterBy指定須要合併的圖片,好比這裏這裏只合並images/icon文件夾下的圖片
      filterBy: function (image) {
        if (image.url.indexOf('/images/icon/') > -1) {
            return Promise.resolve();
        }
        return Promise.reject();
      }
    })
  ]
}
複製代碼

默認會把圖片合併到名爲sprite.png的雪碧圖中。

在css中直接指定小圖標當背景:

.star{
  display: inline-block;
  height: 100px;
  width: 100px;
  &.l1{
    background: url('../icon/star.png') no-repeat;
  }
  &.l2{
    background: url('../icon/star2.png') no-repeat;
  }
  &.l3{
    background: url('../icon/star3.png') no-repeat;
  }
}
複製代碼

打包完成後能夠看到,自動修改了background-imagebackground-position

image.png image.png image.png

gzip

gzip的原理,參考探索HTTP傳輸中gzip壓縮的祕密

開啓gzip壓縮,能夠減少文件體積。在瀏覽器支持gzip的狀況下,能夠加快資源加載速度。服務端和客戶端均可以完成gzip壓縮,服務端響應請求時壓縮,客戶端應用構建時壓縮。但壓縮文件這個過程自己是須要耗費時間和CPU資源的,若是存在大量的壓縮需求,會加大服務器的負擔。

因此能夠在構建打包時候就生成gzip壓縮文件,做爲靜態資源放在服務器上,接收到請求後直接把壓縮文件返回。

使用webpack生成gzip文件須要藉助compression-webpack-plugin,使用配置以下:

const CompressionWebpackPlugin = require("compression-webpack-plugin")
module.exports = {
  plugins: [
     new CompressionWebpackPlugin({
       test: /\.(js|css)$/,         //匹配要壓縮的文件
       algorithm: "gzip"
     })
  ]
}
複製代碼

打包完成後除了生成打包文件外,還會額外生成 .gz後綴的壓縮文件。能夠看出,gzip壓縮文件的體積比未壓縮文件的體積小不少。

image.png

項目地址: github.com/alasolala/w…

相關文章
相關標籤/搜索