15 個 Webpack 優化技巧幫你提升姿勢



01


開發體驗優化

01

優化 Loader 配置

由於 Loader 對文件的轉換操作很耗時,所以需要讓儘可能少的文件被 Loader 處理。可以通過 test/include/exclude 三個配置項來命中 Loader 要應用規則的文件。

        module .exports = {
module : {
rules : [{
//如果項目源碼中只有 文件,就不要寫成/\jsx?$/,以提升正則表達式的性能
test: /\.js$/,
//babel -loader 支持緩存轉換出的結果,通過 cacheDirectory 選項開啓
use: ['babel-loader?cacheDirectory'] ,
//只對項目根目錄下 src 目錄中的文件採用 babel-loader
include: path.resolve(__dirname,'src'),
}],
}
}

webpack 官方文檔中提到: https://www.webpackjs.com/loaders/babel-loader/#babel-loader-%E5%BE%88%E6%85%A2-

02

優化 resolve.modules 配置

resolve.modules 的默認值是['node_modules'],含義是先去當前目錄的 node_modules 目錄下去找我們想找的模塊,如果沒找到就去上一級目錄 ../node_modules 中找,再沒有就去 ../../node_modules 中找,以此類推。這和 Node.js 的模塊尋找機制很相似。當安裝的第三方模塊都放在項目根目錄的 node_modules 目錄下時,就沒有必要按照默認的方式去一層層地尋找,可以指明存放第三方模塊的絕對路徑,以減少尋找.
    module.exports = {
resolve: {
modules: [path.resolve( __dirname,'node modules')]
},
}

03

化 resolve.mainFields 配置

在安裝的第三方模塊中都會有一個 package.json 文件,用於描述這個模塊的屬性,其中可以存在多個字段描述入口文件,原因是某些模塊可以同時用於多個環境中,針對不同的運行環境需要使用不同的代碼。

resolve.mainFields 的默認值和當前的 target 配置有關係,對應的關係如下。

  • target web 或者 webworker 時,值是['browser','module','main']。

  • target 爲其他情況時,值是[ 'module','main']。

以 target 等於 web 爲例, Webpack 會先採用第三方模塊中的 browser 字段去尋找模塊的入口文件,如果不存在,就採用 module 字段,以此類推。

爲了減少搜索步驟,在明確第三方模塊的入口文件描述字段時,我們可以將它設置得儘量少。由於大多數第三方模塊都採用 main 字段去描述入口文件的位置,所以可以這樣配置:

    module.exports = {
resolve: {
//只採用 main 字段作爲入口文件的描述字段,以減少搜索步驟
mainFields: ['main']
}
}

04

優化 resolve.alias 配置

resolve.alias 配置項通過別名來將原導入路徑映射成一個新的導入路徑。

在實戰項目中經常會依賴一些龐大的第三方模塊,以 React 庫爲例,發佈出去的 React 庫中包含兩套代碼

  • 一套是採用 CommonJS 規範的模塊化代碼,這些文件都放在 lib 錄下,以 package.json 中指定的入口文件 react.js 爲模塊的入口

  • 一套是將 React 的所有相關代碼打包好的完整代碼放到一個單獨的文件中, 這些代碼沒有采用模塊化,可以直接執行。其中 dist/react.js 用於開發環境,裏面包含檢查和警告的代碼。dist/react.min.js 用於線上環境,被最小化了。

在默認情況下, Webpack 會從入口文件 ./node_modules/react/react.js 開始遞歸解析和處理依賴的幾十個文件,這會是一個很耗時的操作 通過配置 resolve.alias, 可以讓 Webpack 在處理 React 庫時,直接使用單獨、完整的 react.min.js 文件,從而跳過耗時的遞歸解析操作.

    module.exports = {
resolve: {
//使用 alias 將導入 react 的語句換成直接使用單獨、完整的 react.min.js 文件,
//減少耗時的遞歸解析操作
alias: {
'react': path.resolve( __dirname ,'./node_modules/react/dist/react.min.js'),
}
}
}

但是,對某些庫使用本優化方法後,會影響到使用 Tree-Sharking 去除無效代碼的優化,因爲打包好的完整文件中有部分代碼在我們的項目中可能永遠用不上。一般對整體性比較強的庫採用本方法優化,因爲完整文件中的代碼是一個整體,每一行都是不可或缺的 但是對於一些工具類的庫,則不建議用此方法。

05

優化 resolve.extensions 配置

在導入語句沒帶文件後綴時,Webpack 會自動帶上後綴去嘗試詢問文件是否存在。如果這個列表越長,或者正確的後綴越往後,就會造成嘗試的次數越多,所以resolve .extensions 的配置也會影響到構建的性能 在配置resolve.extensions 時需要遵守 以下幾點,以做到儘可能地優化構建性能。

  • 後綴嘗試列表要儘可能小,不要將項目中不可能存在的情況寫到後綴嘗試列表中。

  • 頻率出現最高的文件後綴要優先放在最前面,以做到儘快退出尋找過程。

  • 在源碼中寫導入語句時,要儘可能帶上後綴 從而可以避免尋找過程。例如在確定的情況下將 require ( './data ') 寫成 require ('./data.json')

        module.exports = {
resolve : {
//儘可能減少後綴嘗試的可能性
extensions : ['js'],
}
}

06

優化 module.noParse 配置

module.noParse 配置項可以讓 Webpack 忽略對部分沒采用模塊化的文件的遞歸解析處理,這樣做的好處是能提高構建性能。原因是一些庫如 jQuery。

        module.exports = {
module: {
noParse: /jquery/,
}
};

07

使用 DllPlugin

DLLPlugin 和 DLLReferencePlugin 用某種方法實現了拆分 bundles,同時還大大提升了構建的速度。

包含大量複用模塊的動態鏈接庫只需被編譯一次,在之後的構建過程中被動態鏈接庫包含的模塊將不會重新編譯,而是直接使用動態鏈接庫中 的代碼 由於動態鏈接庫中大多數包含的是常用的第三方模塊,例如 react、react-dom ,所以只要不升級這些模塊的版本,動態鏈接庫就不用重新編譯。

    https://github.com/webpack/webpack/tree/master/examples/dll-user

module.exports = {
// mode: "development || "production",
plugins: [
new webpack.DllReferencePlugin({
context: path.join(__dirname, "..", "dll"),
manifest: require("../dll/dist/alpha-manifest.json") // eslint-disable-line
}),
new webpack.DllReferencePlugin({
scope: "beta",
manifest: require("../dll/dist/beta-manifest.json"), // eslint-disable-line
extensions: [".js", ".jsx"]
})
]
};

這個理解起來不費勁,操作起來很費勁。所幸,在Webpack5中已經不用它了,而是用HardSourceWebpackPlugin,一樣的優化效果,但是使用卻及其簡單

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')

const plugins = [
new HardSourceWebpackPlugin()
]

更開心的是,這個插件,webapck4就可以用啦,趕緊用起來吧~

注意:該插件與測量各流程耗時的插件speed-measure-webpack-plugin不兼容。

08

使用 HappyPack

Webpack 是單線程模型的,也就是說 Webpack 需要一個一個地處理任務,不能同時處理多個任務。HappyPack將任 務分解給多個子進程去併發執行,子進程處理完後再將結果發送給主進程,從而發揮多核 CPU 電腦的威力。

    const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })

{
test: /\.js$/,
// loader: 'babel-loader',
loader: 'happypack/loader?id=happy-babel-js', // 增加新的HappyPack構建loader
include: [resolve('src')],
exclude: /node_modules/,
}

plugins: [
new HappyPack({
id: 'happy-babel-js',
loaders: ['babel-loader?cacheDirectory=true'],
threadPool: happyThreadPool
})
]

在整個 Webpack 構建流程中,最耗時的流程可能就是 Loader 對文件的轉換操作了,因爲要轉換的文件數據量巨大,而且這些轉換操作都只能一個一個地處理 HappyPack 的核心原理就是將這部分任務分解到多個進程中去並行處理,從而減少總的構建時間。

 09

使用 ParallelUglifyPlugin

webpack默認提供了UglifyJS插件來壓縮JS代碼,但是它使用的是單線程壓縮代碼,也就是說多個js文件需要被壓縮,它需要一個個文件進行壓縮。所以說在正式環境打包壓縮代碼速度非常慢(因爲壓縮JS代碼需要先把代碼解析成用Object抽象表示的AST語法樹,再去應用各種規則分析和處理AST,導致這個過程耗時非常大)。

當webpack有多個JS文件需要輸出和壓縮時候,原來會使用UglifyJS去一個個壓縮並且輸出,但是ParallelUglifyPlugin插件則會開啓多個子進程,把對多個文件壓縮的工作分別給多個子進程去完成,但是每個子進程還是通過UglifyJS去壓縮代碼。無非就是變成了並行處理該壓縮了,並行處理多個子任務,效率會更加的提高。

10

優化文件監的性能

在開啓監聽模式時,默認情況下會監聽配置的 Entry 文件和所有 Entry 遞歸依賴的文件,在這些文件中會有很多存在於 node_modules 下,因爲如今的 Web 項目會依賴大量的第三方模塊, 所以在大多數情況下我們都不可能去編輯 node_modules 下的文件,而是編輯自己建立的源碼文件,而一個很大的優化點就是忽略 node_modules 下的文件,不監聽它們。

    module.export = {
watchOptions : {
//不監聽的 node_modules 目錄下的文件
ignored : /node_modules/,
}
}

採用這種方法優化後, Webpack 消耗的內存和 CPU 將會大大減少。

02


輸出質量優化

01

Webpack 實現 CDN 的接入

總之,構建需要實現以下幾點:

  • 靜態資源的導入  URL 需要變成指向 DNS 服務的絕對路徑的 URL,而不是相對 HTML 文件的

  • 靜態資源的文件名需要帶上由文件內容算出來的 Hash 值,以防止被緩存

  • 將不同類型的資源放到不同域名的 DNS 服務上,以防止資源的並行加載被阻塞

更多參考:http://webpack.wuhaolin.cn/4%E4%BC%98%E5%8C%96/4-9CDN%E5%8A%A0%E9%80%9F.html

02

使用 Tree Shaking

Tree Shaking 正常工作的前提是,提交給 Webpack 的 JavaScript 代碼必須採用了 ES6 的模塊化語法,因爲 ES6 模塊化語法是靜態的,可以進行靜態分析。

首先,爲了將採用 ES6 模塊化的代碼提交給 Webpack ,需要配置 Babel 以讓其保留 ES6 模塊化語句。修改 .babelrc 文件如下:

    {
'presets':[
[
'env',{
'module':false
}
]
]
}

第二個要求,需要使用UglifyJsPlugin插件。如果在mode:"production"模式,這個插件已經默認添加了,如果在其它模式下,可以手工添加它。

另外要記住的是打開optimization.usedExports。在mode: "production"模式下,它也是默認打開了的。它告訴webpack每個模塊明確使用exports。這樣之後,webpack會在打包文件中添加諸如/* unused harmony export */這樣的註釋,其後UglifyJsPlugin插件會對這些註釋作出理解。

    module.exports = {
mode: 'none',
optimization: {
minimize: true,
minimizer: [
new UglifyJsPlugin()
],
usedExports: true,
sideEffects: true
}
}

03

提取公共代碼

大型網站通常由多個頁面組成,每個頁面都是一個獨立的單頁應用,但由於所有頁面都採用同樣的技術棧及同一套樣式代碼,就導致這些頁面之間有很多相同的代碼。可以使用 splitChunks 進行分包:

    splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}

04

分割代碼以按需加載

Webpack 支持兩種動態代碼拆分技術:

  • 符合 ECMAScript proposal 的 import() 語法,推薦使用

  • 傳統的 require.ensure

import() 用於動態加載模塊,其引用的模塊及子模塊會被分割打包成一個獨立的 chunk。Webpack 還允許以註釋的方式傳參,進而更好的生成 chunk。

    // single target
import(
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
'module'
);

// multiple possible targets
import(
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
`./locale/${language}`
);

迴歸到實際業務場景,頁面基本上都是通過路由的方式呈現,如果按照路由的方式實現頁面級的異步加載,豈不是方便很多。例如,react 中可使用 loadable :

    import React from 'react'
import { Route } from 'react-router-dom'
import { loadable } from 'react-common-lib'

const Test = loadable({
loader: () => import('./test'),
})

const AppRouter = () => (
<>
<Route path="/test" exact component={Test} />
</>
)

05

分析工具

官方可視化工具:http://webpack.github.io/analyse/

❤️ 看完三件事

如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)

  2. 關注我的博客 https://github.com/SHERlocked93/blog,讓我們成爲長期關係

  3. 關注公衆號「前端下午茶」,持續爲你推送精選好文,也可以加我爲好友,隨時聊騷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章