常用的webpack優化方法


1. 前言

關於webpack,相信現在的前端開發人員一定不會陌生,因為它已經成為前端開發人員必不可少的一項技能,它的官方介紹如下:

webpack 是一個模塊打包器。webpack的主要目標是將 JavaScript 文件打包在一起,打包后的文件用於在瀏覽器中使用,但它也能夠勝任轉換(transform)、打包(bundle)或包裹(package)任何資源(resource or asset)。

在日常開發工作中,我們除了會使用webpack以及會編寫它的配置文件之外,我們還需要了解一些關於webpack性能優化的方法,這樣在實際工作就能夠如虎添翼,增強自身的競爭力。

關於webpack優化的方法我將其分為兩大類,如下:

  • 可以提高webpack打包速度,減少打包時間的優化方法
  • 可以讓 Webpack 打出來的包體積更小的優化方法

OK,廢話不多說,接下來我們就來分別了解一下優化方法。

2. 提高 Webpack 打包速度

2.1 優化Loader搜索范圍

對於 Loader 來說,影響打包效率首當其沖必屬 Babel 了。因為 Babel 會將代碼轉為字符串生成 AST,然后對 AST 繼續進行轉變最后再生成新的代碼,項目越大,轉換代碼越多,效率就越低。當然了,我們是有辦法優化的。

首先我們可以優化 Loader 的文件搜索范圍,在使用loader時,我們可以指定哪些文件不通過loader處理,或者指定哪些文件通過loader處理。

module.exports = {
  module: {
    rules: [
      {
        // js 文件才使用 babel
        test: /\.js$/,
        use: ['babel-loader'],
        // 只處理src文件夾下面的文件
        include: path.resolve('src'),
        // 不處理node_modules下面的文件
        exclude: /node_modules/
      }
    ]
  }
}

對於 Babel 來說,我們肯定是希望只作用在 JS 代碼上的,然后 node_modules 中使用的代碼都是編譯過的,所以我們也完全沒有必要再去處理一遍。

另外,對於babel-loader,我們還可以將 Babel 編譯過的文件緩存起來,下次只需要編譯更改過的代碼文件即可,這樣可以大幅度加快打包時間。

loader: 'babel-loader?cacheDirectory=true'

2.2 cache-loader緩存loader處理結果

在一些性能開銷較大的 loader 之前添加 cache-loader,以將處理結果緩存到磁盤里,這樣下次打包可以直接使用緩存結果而不需要重新打包。

module.exports = {
  module: {
    rules: [
      {
        // js 文件才使用 babel
        test: /\.js$/,
        use: [
          'cache-loader',
          ...loaders
        ],
      }
    ]
  }
}

那這么說的話,我給每個loder前面都加上cache-loader,然而凡事物極必反,保存和讀取這些緩存文件會有一些時間開銷,所以請只對性能開銷較大的 loader 使用 cache-loader。關於這個cache-loader更詳細的使用方法請參照這里cache-loader

2.3 使用多線程處理打包

受限於Node是單線程運行的,所以 Webpack 在打包的過程中也是單線程的,特別是在執行 Loader 的時候,長時間編譯的任務很多,這樣就會導致等待的情況。那么我們可以使用一些方法將 Loader 的同步執行轉換為並行,這樣就能充分利用系統資源來提高打包速度了。

2.3.1 HappyPack

happypack ,快樂的打包。人如其名,就是能夠讓Webpack把打包任務分解給多個子線程去並發的執行,子線程處理完后再把結果發送給主線程。

module: {
  rules: [
    {
        test: /\.js$/,
        // 把對 .js 文件的處理轉交給 id 為 babel 的 HappyPack 實例
        use: ['happypack/loader?id=babel'],
        exclude: path.resolve(__dirname, 'node_modules'),
    },
    {
        test: /\.css$/,
        // 把對 .css 文件的處理轉交給 id 為 css 的 HappyPack 實例
        use: ['happypack/loader?id=css']
    }
  ]
},
plugins: [
  	new HappyPack({
        id: 'js', //ID是標識符的意思,ID用來代理當前的happypack是用來處理一類特定的文件的
        threads: 4, //你要開啟多少個子進程去處理這一類型的文件
        loaders: [ 'babel-loader' ]
    }),
    new HappyPack({
        id: 'css',
        threads: 2,
        loaders: [ 'style-loader', 'css-loader' ]
    })
]

2.3.2 thread-loader

thread-loader ,在worker 池(worker pool)中運行加載器loader。把thread-loader 放置在其他 loader 之前, 放置在這個 thread-loader 之后的 loader 就會在一個單獨的 worker 池(worker pool)中運行。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          {
              loader: "thread-loader",
              // 有同樣配置的 loader 會共享一個 worker 池(worker pool)
              options: {
                  // 產生的 worker 的數量,默認是 cpu 的核心數
                  workers: 2,

                  // 一個 worker 進程中並行執行工作的數量
                  // 默認為 20
                  workerParallelJobs: 50,

                  // 額外的 node.js 參數
                  workerNodeArgs: ['--max-old-space-size', '1024'],

                  // 閑置時定時刪除 worker 進程
                  // 默認為 500ms
                  // 可以設置為無窮大, 這樣在監視模式(--watch)下可以保持 worker 持續存在
                  poolTimeout: 2000,

                  // 池(pool)分配給 worker 的工作數量
                  // 默認為 200
                  // 降低這個數值會降低總體的效率,但是會提升工作分布更均一
                  poolParallelJobs: 50,

                  // 池(pool)的名稱
                  // 可以修改名稱來創建其余選項都一樣的池(pool)
                  name: "my-pool"
              }
          }, 
          {
              loader:'babel-loader'
          }
        ]
      }
    ]
  }
}

同樣,thread-loader也不是越多越好,也請只在耗時的 loader 上使用。

2.3.3 webpack-parallel-uglify-plugin

Webpack3 中,我們一般使用 UglifyJS 來壓縮代碼,但是這個是單線程運行的,也就是說多個js文件需要被壓縮,它需要一個個文件進行壓縮。所以說在正式環境打包壓縮代碼速度非常慢(因為壓縮JS代碼需要先把代碼解析成AST語法樹,再去應用各種規則分析和處理AST,導致這個過程耗時非常大)。為了加快效率,我們可以使用 webpack-parallel-uglify-plugin 插件,該插件會開啟多個子進程,把對多個文件壓縮的工作分別給多個子進程去完成,但是每個子進程還是通過UglifyJS去壓縮代碼。無非就是變成了並行處理該壓縮了,並行處理多個子任務,提高打包效率。來並行運行 UglifyJS,從而提高效率。

Webpack4 中,我們就不需要以上這些操作了,只需要將 mode 設置為 production 就可以默認開啟以上功能。代碼壓縮也是我們必做的性能優化方案,當然我們不止可以壓縮 JS 代碼,還可以壓縮 HTMLCSS 代碼,並且在壓縮 JS 代碼的過程中,我們還可以通過配置實現比如刪除 console.log 這類代碼的功能。

let ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
    module: {},
    plugins: [
        new ParallelUglifyPlugin({
            workerCount:3,//開啟幾個子進程去並發的執行壓縮。默認是當前運行電腦的cPU核數減去1
            uglifyJs:{
                output:{
                    beautify:false,//不需要格式化
                    comments:false,//不保留注釋
                },
                compress:{
                    warnings:false,//在Uglify]s除沒有用到的代碼時不輸出警告
                    drop_console:true,//刪除所有的console語句,可以兼容ie瀏覽器
                    collapse_vars:true,//內嵌定義了但是只用到一次的變量
                    reduce_vars:true,//取出出現多次但是沒有定義成變量去引用的靜態值
                }
            },
        })
    ]
}

關於該插件更加詳細的用法請參照這里webpack-parallel-uglify-plugin

2.4 DllPlugin&DllReferencePlugin

DllPlugin 可以將特定的類庫提前打包成動態鏈接庫,在一個動態鏈接庫中可以包含給其他模塊調用的函數和數據,把基礎模塊獨立出來打包到單獨的動態連接庫里,當需要導入的模塊在動態連接庫里的時候,模塊不用再次被打包,而是去動態連接庫里獲取。這種方式可以極大的減少打包類庫的次數,只有當類庫更新版本才有需要重新打包,並且也實現了將公共代碼抽離成單獨文件的優化方案。

這里我們可以先將reactreact-dom單獨打包成動態鏈接庫,首先新建一個新的webpack配置文件:webpack.dll.js

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
	// 想統一打包的類庫
    entry:['react','react-dom'],
    output:{
        filename: '[name].dll.js',  //輸出的動態鏈接庫的文件名稱,[name] 代表當前動態鏈接庫的名稱
        path:path.resolve(__dirname,'dll'),  // 輸出的文件都放到 dll 目錄下
        library: '_dll_[name]',//存放動態鏈接庫的全局變量名稱,例如對應 react 來說就是 _dll_react
    },
    plugins:[
        new DllPlugin({
            // 動態鏈接庫的全局變量名稱,需要和 output.library 中保持一致
            // 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值
            // 例如 react.manifest.json 中就有 "name": "_dll_react"
            name: '_dll_[name]',
            // 描述動態鏈接庫的 manifest.json 文件輸出時的文件名稱
            path: path.join(__dirname, 'dll', '[name].manifest.json')
        })
    ]
}

然后我們需要執行這個配置文件生成依賴文件:

webpack --config webpack.dll.js --mode development

接下來我們需要使用 DllReferencePlugin 將依賴文件引入項目中

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
module.exports = {
  // ...省略其他配置
  plugins: [
    new DllReferencePlugin({
      // manifest 就是之前打包出來的 json 文件
      manifest:path.join(__dirname, 'dll', 'react.manifest.json')
    })
  ]
}

2.5 noParse

module.noParse 屬性,可以用於配置那些模塊文件的內容不需要進行解析(即無依賴) 的第三方大型類庫(例如jquery,lodash)等,使用該屬性讓 Webpack 不掃描該文件,以提高整體的構建速度。

module.exports = {
    module: {
      noParse: /jquery|lodash/, // 正則表達式
      // 或者使用函數
      noParse(content) {
        return /jquery|lodash/.test(content)
      }
    }
}

2.6 IgnorePlugin

IgnorePlugin用於忽略某些特定的模塊,讓 webpack 不把這些指定的模塊打包進去。

module.exports = {
  // ...省略其他配置
  plugins: [
    new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
  ]
}

webpack.IgnorePlugin()參數中第一個參數是匹配引入模塊路徑的正則表達式,第二個參數是匹配模塊的對應上下文,即所在目錄名。

2.7 打包文件分析工具

webpack-bundle-analyzer插件的功能是可以生成代碼分析報告,幫助提升代碼質量和網站性能。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
      plugins: [
          new BundleAnalyzerPlugin({
            generateStatsFile: true, // 是否生成stats.json文件
          })  
        // 默認配置的具體配置項
        // new BundleAnalyzerPlugin({
        //   analyzerMode: 'server',
        //   analyzerHost: '127.0.0.1',
        //   analyzerPort: '8888',
        //   reportFilename: 'report.html',
        //   defaultSizes: 'parsed',
        //   openAnalyzer: true,
        //   generateStatsFile: false,
        //   statsFilename: 'stats.json', 
        //   statsOptions: null,
        //   excludeAssets: null,
        //   logLevel: info
        // })
  ]
}

使用方式:

"generateAnalyzFile": "webpack --profile --json > stats.json", // 生成分析文件
"analyz": "webpack-bundle-analyzer --port 8888 ./dist/stats.json" // 啟動展示打包報告的http服務器

2.8 費時分析

speed-measure-webpack-plugin,打包速度測量插件。這個插件可以測量webpack構建速度,可以測量打包過程中每一步所消耗的時間,然后讓我們可以有針對的去優化代碼。

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
// 用smw.wrap()包裹webpack的所有配置項
module.exports =smw.wrap({
    module: {},
    plugins: []
});

2.9 一些小的優化點

我們還可以通過一些小的優化點來加快打包速度

  • resolve.extensions:用來表明文件后綴列表,默認查找順序是 ['.js', '.json'],如果你的導入文件沒有添加后綴就會按照這個順序查找文件。我們應該盡可能減少后綴列表長度,然后將出現頻率高的后綴排在前面
  • resolve.alias:可以通過別名的方式來映射一個路徑,能讓 Webpack 更快找到路徑
module.exports ={
    // ...省略其他配置
    resolve: {
        extensions: [".js",".jsx",".json",".css"],
        alias:{
            "jquery":jquery
        }
    }
};

3. 減少 Webpack 打包后的文件體積

3.1 對圖片進行壓縮和優化

image-webpack-loader這個loder可以幫助我們對打包后的圖片進行壓縮和優化,例如降低圖片分辨率,壓縮圖片體積等。

module.exports ={
    // ...省略其他配置
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
                use: [
                    'file-loader',
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            mozjpeg: {
                                progressive: true,
                                quality: 65
                            },
                            optipng: {
                                enabled: false,
                            },
                            pngquant: {
                                quality: '65-90',
                                speed: 4
                            },
                            gifsicle: {
                                interlaced: false,
                            },
                            webp: {
                                quality: 75
                            }
                        }
                    }
                ]
            }
        ]
    }
};

3.2 刪除無用的CSS樣式

有時候一些時間久遠的項目,可能會存在一些CSS樣式被迭代廢棄,需要將其剔除掉,此時就可以使用purgecss-webpack-plugin插件,該插件可以去除未使用的CSS,一般與 globglob-all 配合使用。

注意:此插件必須和CSS代碼抽離插件mini-css-extract-plugin配合使用。

例如我們有樣式文件style.css

body{
    background: red
}
.class1{
    background: red
}

這里的.class1顯然是無用的,我們可以搜索src目錄下的文件,刪除無用的樣式。

const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');

module.exports ={
    // ...
    plugins: [
        // 需要配合mini-css-extract-plugin插件
        new PurgecssPlugin({
            paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, 
                  {nodir: true}), // 不匹配目錄,只匹配文件
            })
        }),
    ]
}

3.3 以CDN方式加載資源

我們知道,一般常用的類庫都會發布在CDN上,因此,我們可以在項目中以CDN的方式加載資源,這樣我們就不用對資源進行打包,可以大大減少打包后的文件體積。

CDN方式加載資源需要使用到add-asset-html-cdn-webpack-plugin插件。我們以CDN方式加載jquery為例:

const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')

module.exports ={
    // ...
    plugins: [
        new AddAssetHtmlCdnPlugin(true,{
            'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
        })
    ],
    //在配置文件中標注jquery是外部的,這樣打包時就不會將jquery進行打包了
    externals:{
      'jquery':'$'
    }
}

3.4 開啟Tree Shaking

Tree-shaking,搖晃樹。顧名思義就是當我們搖晃樹的時候,樹上干枯的沒用的葉子就會掉下來。類比到我們的代碼中就是將沒用的代碼搖晃下來,從而實現刪除代碼中未被引用的代碼。

這個功能在webpack4中,當我們將mode設置為production時,會自動進行tree-shaking

來看下面代碼:

main.js

import { minus } from "./calc";
console.log(minus(1,1));

calc.js

import {test} from './test';
export const sum = (a, b) => {
  return a + b + 'sum';
};
export const minus = (a, b) => {
  return a - b + 'minus';
};

test.js

export const test = ()=>{
    console.log('hello')
}
console.log(test());

觀察上述代碼其實我們主要使用minus方法,test.js代碼是有副作用的!所謂"副作用",官方文檔如下解釋:

「副作用」的定義是,在導入時會執行特殊行為的代碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全局作用域,並且通常不提供 export。

對上述代碼進行打包后發現'hello'依然會被打印出來,這時候我們需要在package.json中配置配置不使用副作用:

{
  "sideEffects": false
}

如果這樣設置,默認就不會導入css文件啦,因為我們引入css也是通過import './style.css'

這里重點就來了,tree-shaking主要針對es6模塊,我們可以使用require語法導入css,但是這樣用起來有點格格不入,所以我們可以配置css文件不是副作用,如下:

{
    "sideEffects":[
        "**/*.css"
    ]
}

3.5 開啟Scope Hoisting

Scope Hoisting 可以讓 Webpack 打包出來的代碼文件更小、運行的更快, 它又譯作 "作用域提升",是在 Webpack3 中新推出的功能。

由於最初的webpack轉換后的模塊會包裹上一層函數,import會轉換成require,因為函數會產生大量的作用域,運行時創建的函數作用域越多,內存開銷越大。而Scope Hoisting 會分析出模塊之間的依賴關系,盡可能的把打包出來的模塊合並到一個函數中去,然后適當地重命名一些變量以防止命名沖突。這個功能在webpack4中,當我們將mode設置為production時會自動開啟。

比如我們希望打包兩個文件

let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;
// 引入d
import d from './d';
console.log(d)

最終打包后的結果會變成 console.log(6),這樣的打包方式生成的代碼明顯比之前的少多了,並且減少多個函數后內存占用也將減少。如果你希望在開發模式development中開啟這個功能,只需要使用插件 webpack.optimize.ModuleConcatenationPlugin() 就可以了。

module.exports = {
  // ...
  plugins: [
    // 開啟 Scope Hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
  ]
}

3.6 按需加載&動態加載

必大家在開發單頁面應用項目的時候,項目中都會存在十幾甚至更多的路由頁面。如果我們將這些頁面全部打包進一個文件的話,雖然將多個請求合並了,但是同樣也加載了很多並不需要的代碼,耗費了更長的時間。那么為了首頁能更快地呈現給用戶,我們肯定是希望首頁能加載的文件體積越小越好,這時候我們就可以使用按需加載,將每個路由頁面單獨打包為一個文件。在給單頁應用做按需加載優化時,一般采用以下原則:

  • 對網站功能進行划分,每一類一個chunk
  • 對於首次打開頁面需要的功能直接加載,盡快展示給用戶,某些依賴大量代碼的功能點可以按需加載
  • 被分割出去的代碼需要一個按需加載的時機

動態加載目前並沒有原生支持,需要babel的插件:plugin-syntax-dynamic-import。安裝此插件並且在.babelrc中配置:

{
  // 添加
  "plugins": ["transform-vue-jsx", "transform-runtime"],
  
}

例如如下示例:

index.js

let btn = document.createElement('button');
btn.innerHTML = '點擊加載視頻';
btn.addEventListener('click',()=>{
    import(/* webpackChunkName: "video" */'./video').then(res=>{
        console.log(res.default);
    });
});
document.body.appendChild(btn);

webpack.config.js

module.exports = {
    // ...
    output:{
      chunkFilename:'[name].min.js'
    }
}

這樣打包后的結果最終的文件就是 video.min.js,並且剛啟動項目時不會加載該文件,只有當用戶點擊了按鈕時才會動態加載該文件。

4. 總結

以上就是一些常用的webpack優化手段,當然webpack優化手段還有很多,並且用法也有很多。需要的話可以閱讀官方文檔來深入學習。

(完)


免責聲明!

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



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