webpack效能優化(上)

wdBly發表於2019-03-03

webpack做為一款當下最流行的前端構建工具,一直以來以門檻太高而受人詬病,且沒有一個通用的配置適合所有的專案,為此我們不得不開心的(大誤:smiley:)手動配置我們的專案,開啟我們的配置工程師之路,本文主要是對webpack的一些基礎配置的解釋和我們能做的一些優化工作,適合有使用過webpack的前端開發者閱讀,各位在閱讀過程中不妨開啟自己的專案來跟著我一起優化。

分割配置檔案 dev prod

通常我們的專案會有開發環境和生產環境,而開發環境我們配置的目標是構建更快,模組熱替換,能從chrome控制檯報錯資訊對應的原始碼的錯誤處(source map)等。生產環境我們更加關注chunk分離,快取,安全,tree shaking等優化點。
當然對於開發和生產環境的配置檔案肯定是不完全相同的,所以我們將不同環境的配置檔案分離開,同時將共用部分抽出來,最後使用webpack-merge外掛整合。

//參考使用方法
const webpackMerge = require(`webpack-merge`);
const additionalConfigPath = {
    development: `./webpack.dev.config.js`,
    production: `./webpack.prod.config.js`
};
const baseConfig = {};
module.exports = webpackMerge(
    baseConfig,
    require(additionalConfigPath[process.env.NODE_ENV])
);

複製程式碼

在下方的優化點中 我會標註這些點適用的環境 dev=開發環境 prod=生產環境

程式碼分離 prod

程式碼分離能夠將工程程式碼分離到各個檔案中,然後按需載入或並行載入這些檔案,也用於獲取更小的 bundle,以及控制資源載入優先順序

webpack有三種常用的程式碼分離方法:

  • 入口起點:配置多入口,輸出多個chunk。
  • 防止重複:使用 CommonsChunkPlugin 去重和分離 chunk(webpack4雖然廢棄,但有代替方法)
  • 動態匯入:常用於按需載入的模組分離

入口起點


//多入口配置 最終輸出兩個chunk
module.exports = {
  entry: {
    index: `index.js`,
    module: `module.js`
  },
  output: {
    //對於多入口配置需要指定[name]否則會出現重名問題
    filename: `[name].bundle.js`,
    path: path.resolve(__dirname, `dist`)
  }
};

複製程式碼

問題所在:入口chunks中如果包含重複的模組,如jquery,那些重複模組都會被引入到各個bundle中

去除重複

對於webpack4 < 4 我們使用CommonsChunkPlugin外掛


entry: {
    "app": "entry-client.js"
}

//首先將app入口的公共模組提取出來
new webpack.optimize.CommonsChunkPlugin({
    name: `common`,
    filename: `[name].[chunkHash:6].common.js`,
    chunks: [`app`]
})

//將 vendor runtime分離出來
new webpack.optimize.CommonsChunkPlugin({
    name: [`vendor`,`runtime`],
    filename: `[name].[chunkHash:6].bundle.js`,
    //這個配置保證沒其它的模組會打包進 vendor chunk
    minChunks: Infinity
}),

複製程式碼

chunks 選項代表了從哪個入口分離公共檔案,這裡我們使用我們專案的主入口 app來分離自定義的公共模組

首先將自定義的公共模組單獨抽離出來,這樣做的目的是方便我們做快取,當自定義模組更新時不會影響到vendor檔案的hash值。

然後將第三方庫檔案 和 webpack執行檔案分離。

這樣我們的vendor就是非常乾淨的了,只包含第三方庫,可以做長效快取。這個地方需要分離webpack執行檔案runtime,因為無論我們是否修改了專案檔案,每次build專案時的runtime檔案的hash值總是會發生變化的,需要單獨打出來。

對於webpack4 我們需要實現同樣的目標


// 提前自定義公共模組
optimization: {
    splitChunks: {
        chunks: "all",
        minSize: 20000,
	//其他入口chunk引用的次數
        minChunks: 1, 
	//預設使用name + hash生成檔名
        name: true,
        //使用自定義快取組
        cacheGroups: {
            //公共模組
            commons: {
                name: `common`,
                //快取優先順序設定
                priority: 10,
		//從入口chunk提取
                chunks: `initial`
            },
            //提取第三方庫
	    vendors: {
                //符合條件的放入當前快取組
                test: /[\/]node_modules[\/]/,
                name: "vendors",
                chunks: "all"
            },
        }
    }
}

//提取webpack執行檔案
runtimeChunk: {
    name: `manifest`
},

複製程式碼

上面的配置大概意思是對所有滿足條件的chunk開啟程式碼拆分。

優先順序(priority):多個分組衝突時決定把程式碼放在哪塊

通過splitChunks 和 runtimeChunk外掛我們能達到同樣的拆分和快取目的

路由元件動態匯入

webpack可以將非同步載入的元件打包為chunk,同時按需載入此chunk。最為常見是我們的路由元件,我們可以將路由入口元件動態匯入以達到非同步載入的目的。

非同步元件的匯入主要有兩種方式
1:ES6 stage3的提案 import()
2:webpack 特定的 require.ensure

//import 方式 
const Login = () => {
    import(/* webpackChunkName: `Login` */`./Login`)
}

//require.ensure 方式 
require.ensure([], function(require){
    require(`./Login`);
},"Login");

//非同步元件打包的chunk命名 使用webpackChunkName 或者
//使用require.ensure的第三個引數

//對於webpack2/3需要配置chunkFilename來接收配置的name
output: {
    //非入口 chunk 的名稱
    chunkFilename: `[name].[chunkHash:6].chunk.js`
}

//webpack4配置name為true(預設值)
optimization: {
    splitChunks: {
	//chunk name由塊名和hash值自動生成
        name: true
    }
}

複製程式碼

require.ensure 現在依賴於原生的 Promise。如果在不支援 Promise 的環境裡使用 require.ensure,你需要新增 polyfill。

普通元件的非同步載入

既然我們可以使用import()和require.ensure來動態匯入元件那其實在專案中我們更是做到可以基於元件的非同步載入。

比如我們的首頁是由20個元件組成,但是首屏只會顯示15個元件,另外的5個元件可能是一些彈窗,或底部才顯示的這種元件,那麼我們完全可以將之使用動態匯入抽離出來,在需要的時候載入它,如:


//在非同步事件中去匯入元件
<div onClick={this.showPopUps}>
    show-popUps
</div>

showPopUps = () => {
  this.popUps = ()=> {
    import(/*webpackChunkName: `PopUps`*/`./popUps`)
  }
}

複製程式碼

這樣處理的問題在於,我們在點選按鈕彈出浮層時會有一定的延時,因為在點選時我們才從服務端去獲取元件,使用者體驗可能不太優秀。

這時我們可以使用一種預測載入的技術prefetch。prefetch提示瀏覽器這個資源將來可能需要,瀏覽器會選擇==空閒的時間==去下載此資源。prefetch常常用於加速下一次操作。

<link rel="prefetch">
複製程式碼

相容性問題:從上述可以得知,prefetch時基於瀏覽器實現的,存在一定的相容性問題,在safari上還沒由得到支援。

在不支援的瀏覽器中並不會影響我們頁面的功能

使用:preload-webpack-plugin
使用條件:webpack > 2.2 且需要使用 html-webpack-plugin


plugins: [
    new HtmlWebpackPlugin(),
    new PreloadWebpackPlugin({
	rel: "prefetch",
	as: `script`,
        //包含了哪些chunk,預設值為"asyncChunks"
        include: `asyncChunks`
    })
]

//最終效果類似這樣
<link rel="prefetch" as="script" href="chunk.d15e.js">

複製程式碼

對於.css結尾的檔案 as=style,.woff2結尾的檔案as=font,否則as=script,當然你可以像上方那樣給一個確定的值。

對於include的值,其實我們並不是需要所有的非同步chunk都使用prefetch,所以我們需要重設include的值,像這樣:


//1 給定需要prefetch的塊的名字(chunkName)
include: ["PopUps","Login"]

//2 或者在動態匯入時指定Prefetch
this.popUps = () => {
    import(/* webpackChunkName: `PopUps`, webpackPrefetch: true */`./PopUps`)
}

複製程式碼

Loader dev prod

為Loader指定作用範圍可以加快我們的構建速度,提升開發體驗。

{
    test: /.jsx?$/,
    use: [
        "babel-loader"
    ],
    exclude: /node_modules/
}
//or
{
    test: /.jsx?$/,
    use: [
        "babel-loader"
    ],
    include: /src/
}

複製程式碼

webpack解析(resolve) dev

resolve的部分配置過多項,會導致webpack搜尋範圍變大,效率下降,如沒有特殊需求,可以保持預設配置保持開發體驗。

//解析模組時應該搜尋的目錄
modules: ["node_modules"] //default

//自動解析確定的擴充套件,不宜過多
extensions: [".js", ".json"] //default

//解析目錄時要使用的檔名,不宜過多
mainFiles: ["index"] //default
複製程式碼

webpack 外部擴充套件(Externals) dev prod

externals 配置選項提供了「從輸出的 bundle 中排除依賴」的方法,什麼意思呢?顧名思義,externals可以排除掉我們打包結果中某些依賴:

//排除react 我們打包後的bundle中沒有react了
externals: {
    //將react指向一個全域性變數 react
    react: "react"
}

//這裡的 "react" 會直接去全域性變數中尋找
import react from "react"


複製程式碼

這種設定通常用於==library==開發人員,你的library依賴了react,同時使用了你的庫的開發人員也依賴了react,最終會造依賴你的庫的使用者bundle中react被重複打包。

你應該做的是在你的library中將外部依賴放到externals中排除掉,然後將你的依賴暴露給使用你的庫的使用者,最後你的library應該從外部獲取這些擴充套件依賴,而不是一股腦打包進你自己的bundle中。當然這個依賴在使用者的環境中必須存在且可用。

問題所在:對於從一個依賴目錄中,呼叫的多個檔案,使用externals 無法一次排除;

import one from `react/one`;
import two from `react/two`;

//上面這種情況只能一個個匹配,或者這通過正則匹配
externals: [
  `react/one`,
  `react/two`,
  /^react/.+$/
]
複製程式碼

這裡不得不提的是npm依賴管理中的peerDependencies,它的作用正是將我們library中依賴的庫暴露給使用者的,若是使用者沒有安裝我們庫中的必需依賴,npm會丟擲錯誤。

當然如果你不是一個library開發人員,你同樣可以使用externals來排除專案中的外部依賴,然後通過其他方式引入成為一個全域性變數的方式來優化打包速度。

Dlls 優化構建速度 dev

webpack在每次build時都會將整個專案重新構建一遍,不管這個檔案是否發生了改變(當然若檔案未更改hash值不變),但是其實我們所依賴的三方庫更改並不頻繁,若是將三方庫抽離出來單獨構建,將構建好的目標和構建生成的json對照檔案引如我們的專案,這樣不用每次都去打包這些程式碼。

使用 DllPlugin 將更改不頻繁的程式碼進行單獨編譯。這將改善引用程式的編譯速度,即使它增加了構建過程的複雜性。


//新建 webpack.dll.conf.js
module.exports = {
    entry: {
        vendor: [react, ...]
    },
    output: {
        filename: `dll.[name].js`
    },
    plugins: [
        new webpack.DllPlugin({
            path: `[name]-manifest.json`
        })
    ]
}

//webpack.base.conf.js
const manifest = require(`./vendor-manifest.json`)

plugins: [
    new webpack.DllReferencePlugin({
        manifest
    })
]

複製程式碼

最後需要手動將生成的dll.[name].js引入到html檔案中去,當然這一步驟可以用assets-webpack-plugin外掛優化。

webpack效能優化(下)

首發原文地址

相關文章