Webpack編譯速度優化實踐

當咱們的應用還處於小規模的時候,咱們可能不會在意Webpack的編譯速度,不管使用3.X仍是4.X版本,它都足夠快,或者說至少沒讓你等得不耐煩。但隨着業務的增多,嗖嗖嗖一下項目就有上百個組件了,也是件很簡單的事情。這時候當你再獨立編前端模塊的生產包時,或者CI工具中編整個項目的包時,若是Webpackp配置沒通過優化,那編譯速度都會慢得一塌糊塗。編譯耗時十多秒的和編譯耗時一兩分鐘的體驗是迥然不一樣的。從開發期間作臨時部署時的效率和CI的總體效率這兩點出發,咱們都有必要去加快前端項目的編譯速度。css

本文基於筆者在月初對項目編譯速度的優化實踐,從工具鏈使用的角度介紹下基於Webpack的項目可作編譯速度優化的地方。文中全部的可配置項和工具都由Webpack平臺或者社區的工具提供,筆者對它們API的使用上不會作太多講解,須要的同窗能夠直接翻看對應的文檔。筆者的Webpack版本爲4.29.6,後文中的內容都基於這個版本。html

 

1、已存在的針對編譯速度的優化前端

筆者這套Webpack架子源自CRA種子腳手架的eject,基於Webpack4.x,在對不一樣資源模塊的處理上已經是最佳實踐的方案,在這方面基本上無需改動什麼。但在編譯速度優化上較弱,在項目規模擴大後,劣勢很是明顯。vue

其原有的針對編譯的優化配置有這三處:node

1. 經過terser-webpack-plugin的parallel和cache配置來並行處理並緩存以前的編譯結果。terser-webpack-plugin是以前UglifyPlugin的一個替代品,由於UglifyPlugin已經沒有繼續維護了,從Webpack4.x起,已經推薦使用terser-webpack-plugin來進行代碼壓縮、混淆,以及Dead Code Elimination以實現Tree Shaking。對於parallel從整個設置的名稱你們就會知道它有什麼用,沒錯,就是並行,而cache也就是緩存該插件的處理結果,在下一次的編譯中對於內容未改變的文件能夠直接複用上一次編譯的結果。react

2. 經過babel-loader的cache配置來緩存babel的編譯結果。jquery

3. 經過IgnorePlugin設置對moment的整個locale本地化文件夾導入的正則匹配,來防止將全部的本地化文件進行打包。若是你確實須要某國語言,僅手動導入那國的語言包便可。webpack

在項目逐漸變大的過程當中,生產包的編譯時間也從十幾秒增加到了一分多鐘,這是讓人受不了的,這就迫使着筆者必須進行額外的優化以加快編譯速度,爲編包節省時間。下面的段落就講解下筆者作的幾個額外優化。git

 

2、多線程(多進程模擬)支持web

從上個段落的terser-webpack-plugin的parallel設置中,咱們能夠獲得這個啓發:啓用多進程來模擬多線程(由於一個Node.js進程是單線程的),並行處理資源的編譯。因而筆者引入了HappyPack,筆者以前的那套老架子也用了它,但以前沒寫東西來介紹那套架子,這裏就一併說了。關於HappyPack,常常玩Webpack的同窗應該不會陌生,網上也有一些關於其原理是使用的介紹文章,也寫得很不錯。HappyPack的工做原理大體就是在Webpack和Loader之間多加了一層,改爲了Webpack並非直接去和某個Loader進行工做,而是Webpack test到了須要編譯的某個類型的資源模塊後,將該資源的處理任務交給了HappyPack,由HappyPack在內部線程池中進行任務調度,分配一個線程調用處理該類型資源的Loader來處理這個資源,完成後上報處理結果,最後HappyPack把處理結果返回給Webpack,最後由Webpack輸出到目的路徑。經過這一系列操做,將本來都在一個Node.js線程內的工做,分配到了不一樣的線程(進程)中並行處理。

使用方法以下:

首先引入HappyPack並建立線程池:

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

替換以前的Loader爲HappyPack的插件:

{
    test: /\.(js|mjs|jsx|ts|tsx)$/,
    include: paths.appSrc,
    use: ['happypack/loader?id=babel-application-js'],
},

將原Loader中的配置,移動到對應插件中:

new HappyPack({
    id: 'babel-application-js',
    threadPool: happyThreadPool,
    verbose: true,
    loaders: [
        {
            loader: require.resolve('babel-loader'),
            options: {
                ...省略
            },
        },
    ],
}),

大體使用方式如上所示,HappyPack的配置講解文章有不少,不會配的同窗能夠本身搜索,本文這裏只是順帶說說而已。

HappyPack老早也沒有維護了,它對url-loader的處理是有問題的,會致使通過url-loader處理的圖片都無效,筆者以前也去提過一個Issue,有別的開發者也發現過這個問題。總之,用的時候必定要測試一下。

對於多線程的優點,咱們舉個例子:
好比咱們有四個任務,命名爲A、B、C、D。

任務A:耗時5秒

任務B:耗時7秒

任務C:耗時4秒

任務D:耗時6秒

單線程串行處理的總耗時大約在22秒。

改爲多線程並行處理後,總耗時大約在7秒,也就是那個最耗時的任務B的執行時長,僅僅經過配置多線程處理咱們就能獲得大幅的編譯速度提高。

寫到這裏,你們是否是以爲編譯速度優化就能夠到此結束了?哈哈,固然不是,上面這個例子在實際的項目中根本不具備普遍的表明性,筆者實際項目的狀況是這樣的:

咱們有四個任務,命名爲A、B、C、D。

任務A:耗時5秒

任務B:耗時60秒

任務C:耗時4秒

任務D:耗時6秒

單線程串行處理的總耗時大約在75秒。

改爲多線程並行處理後,總耗時大約在60秒,從75秒優化到60秒,確實有速度上的提高,可是由於任務B的耗時太長了,致使整個項目的編譯速度並無發生本質上的變化。事實上筆者以前那套Webpack3.X的架子就是由於這個問題致使編譯速度慢,在大多數項目中也都是這個狀況,處理某種資源類型的Loader的執行時間很是長,遠遠超過了其餘Loader的執行時間。因此,只靠引入多線程編譯就想解決大項目編譯速度慢的問題是不現實的

那咱們還有什麼辦法嗎?固然有,咱們仍是能夠從TerserPlugin獲得靈感,那就是依靠緩存:在下一次的編譯中可以複用上一次的結果而不執行編譯永遠是最快的。

至少存在有這三種方式,可讓咱們在執行構建時不進行某些文件的編譯,從最本質上提高前端項目總體的構建速度:

1. 相似於terser-webpack-plugin的cache那種方式,這個插件的cache默認生成在node_modules/.cache/terser-plugin文件下,經過SHA或者base64編碼以前的文件處理結果,並保存文件映射關係,方便下一次處理文件時能夠查看以前同文件(同內容)是否有可用緩存。其餘Webpack平臺的工具也有相似功能,但緩存方式不必定相同。

2. 經過externals配置在編譯的時候直接忽略掉外部庫的依賴,不對它們進行編譯,而是在運行的時候,經過<script>標籤直接從CDN服務器下載這些庫的生產環境文件。

3. 將某些能夠庫文件編譯之後保存起來,每次編譯的時候直接跳過它們,但在最終編譯後的代碼中可以引用到它們,這就是Webpack DLLPlugin所作的工做,DLL借鑑至Windows動態連接庫的概念。

後面的段落將針對這幾種方式作講解。

 

3、Loader的Cache

除了段落一中提到的terser-webpack-plugin和babel-loader支持cache外,Webpack還直接另外提供了一種能夠用來緩存前序Loader處理結果的Loader,它就是cache-loader。一般咱們能夠將耗時的Loader都經過cache-laoder來緩存編譯結果。好比咱們打生產環境的包,對於Less文件的緩存你能夠這樣使用它:

{
    test: /\.less$/,
    use: [
        {
            loader: MiniCssExtractPlugin.loader,
            options: {
                ...省略
            },
        },
        {
            loader: 'cache-loader',
            options: {
                cacheDirectory: paths.appPackCacheCSS,
            }
        },
        { 
            loader: require.resolve('css-loader'),
            options: {
                ...省略
            },
        },
        {
            loader: require.resolve('postcss-loader'),
            options: {
                ...省略
            }
        }
    ]
}

Loader的執行順序是從下至上,所以經過上述配置,咱們能夠經過cache-laoder緩存postcss-loader和css-loader的編譯結果。

但咱們不能用cache-loader去緩存mini-css-extract-plugin的結果,由於它的做用是要從前序Loader編譯成的含有樣式字符串的JS文件中把樣式字符串單獨抽出來打成獨立的CSS文件,而緩存這些獨立CSS文件並非cache-loader的工做。

但若是是要緩存開發環境的Less編譯結果,cache-loader能夠緩存style-loader的結果,由於style-loader並無從JS文件中單獨抽出樣式代碼,只是在編譯後的代碼中添加了一些額外代碼,讓編譯後的代碼在運行時,可以建立包含樣式的<style>標籤並放入<head>標籤內,這樣的性能不是太好,因此基本上只有開發環境採用這種方式。

在對樣式文件配置cache-loader的時候,必定要記住上述這兩點,要否則會出現樣式沒法正確編譯的問題。

除了對樣式文件的編譯結果進行緩存外,對其餘類型的文件(除了會打包成獨立的文件外)的編譯結果進行緩存也是能夠的。好比url-laoder,只要大小沒有達到limitation的圖片都會被打成base64,大於limitation的文件會打成單獨的圖片類文件,就不能被cache-loader緩存了,若是遇到了這種狀況,資源請求會404,這是在使用cache-loader時須要注意的。

固然,經過使用緩存能獲得顯著編譯速度提高的,依舊是那些耗時的Loader,若是對某些類型的文件編譯並不耗時,或者說文件自己數量太少,均可以先沒必要作緩存,由於即使作了緩存,編譯速度的提高也不明顯。

最後筆者將全部Loader和Plugin的cache默認目錄從node_modules/.cache/移到了項目根目錄的build_pack_cache/目錄(生產環境)和dev_pack_cache目錄(開發環境),經過NODE_ENV自動區分。這麼作是由於筆者的CI工程每次會刪除以前的node_modules文件夾,並從node_modules.tar.gz解壓一個新的node_modules文件夾,因此將緩存放在node_modules/.cache/目錄裏面會無效,CI的這套邏輯對於前段項目來講是比較坑的,筆者也不想去動CI的代碼。通過過這個改動後,對cache文件的管理更直觀一些,也能避免node_modules的體積一直增大。若是想清除緩存,直接刪掉對應目錄便可。固然了,這兩個目錄是不須要被Git跟蹤的,因此須要在.gitignore中添加上。CI環境中若是沒有對應的緩存目錄,相關Loader會自動建立。這樣作還考慮到這個緣由:由於開發環境和生產環境編譯出的資源是不一樣的,在開發環境下對資源的編譯每每都沒有作壓縮和混淆處理等,所以即便某個源文件的源碼沒變,其在開發環境下和生產環境下的緩存也是不能通用的還會相互覆蓋。因此爲了有效地緩存不一樣環境下的編譯結果,是有必要區分開不一樣編譯環境下的緩存目錄的。

 

4、外部擴展externals

按照Webpack官方的說法:咱們的項目若是想用一個庫,但咱們又不想Webpack對它進行編譯(由於它的源碼極可能已經是通過編譯和優化的生產包,能夠直接使用)。而且咱們可能經過window全局方式來訪問它,或者經過各類模塊化的方式來訪問它,那麼咱們就能夠把它配置進extenals裏。

好比我要使用jquery能夠這樣配置:

externals: {
    jquery: 'jQuery'
}

我就能夠這樣使用了,就像咱們直接引入一個在node_modules中的包同樣:

import $ from 'jquery';

$('.div').hide();

這樣作能有效的前提就是咱們在HTML文件中在上述代碼執行之前就已經經過了<script>標籤從CDN下載了咱們須要的依賴庫了,externals配置會自動在承載咱們應用的html文件中加入:

<script src="https://code.jquery.com/jquery-1.1.14.js">

externals還支持其餘靈活的配置語法,好比我只想訪問庫中的某些方法,咱們甚至能夠把這些方法附加到window對象上:

externals : {
    subtract : {
        root: ["math", "subtract"]
    }
}

我就能夠經過 window.math.subtract 來訪問subtract方法了。

對於其餘配置方式若是有興趣的話能夠自行查看文檔。

可是,筆者的項目並無這麼作,由於在它最終交付給客戶後,應該是處於一個內網環境(或者一個被防火牆嚴重限制的環境)中,極大可能沒法訪問任何互聯網資源,所以經過<script>腳本請求CDN資源的方式將失效,前置依賴沒法正常下載就會致使整個應用奔潰。

 

5、DllPlugin

在上個段落中的結尾處,提到了筆者的項目在交付用戶後會面臨的網絡困境,因此筆者必須選擇另一個方式來實現相似於externals配置可以提供的功能。那就是Webpack DLLPlugin以及它的好搭檔DLLReferencePlugin。筆者有關DLLPlugin的使用都是在構建生產包的時候使用。

要使用DLLPlugiin,咱們須要單獨開一個webpack配置,暫且將其命名爲webpack.dll.config.js,以便和主Webpack的配置文件webpack.config.js進行區分。內容以下:

'use strict';
process.env.NODE_ENV = 'production';
const webpack = require('webpack');
const path = require('path');
const {dll} = require('./dll');
const DllPlugin = require('webpack/lib/DllPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const getClientEnvironment = require('./env');
const paths = require('./paths');
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

module.exports = function (webpackEnv = 'production') {
    const isEnvDevelopment = webpackEnv === 'development';
    const isEnvProduction = webpackEnv === 'production';

    const publicPath = isEnvProduction ? paths.servedPath : isEnvDevelopment && '/';

    const publicUrl = isEnvProduction ? publicPath.slice(0, -1) : isEnvDevelopment && '';
    const env = getClientEnvironment(publicUrl);

    return {
        mode: isEnvProduction ?
            'production' :
            isEnvDevelopment && 'development',
        devtool: isEnvProduction ?
            'source-map' :
            isEnvDevelopment && 'cheap-module-source-map',
        entry: dll,
        output: {
            path: isEnvProduction ? paths.appBuildDll : undefined,
            filename: '[name].dll.js',
            library: '[name]_dll_[hash]'
        },
        optimization: {
            minimize: isEnvProduction,
            minimizer: [
                ...省略
            ]
        },
        plugins: [
            new webpack.DefinePlugin(env.stringified),
            new DllPlugin({
                context: path.resolve(__dirname),
                path: path.resolve(paths.appBuildDll, '[name].manifest.json'),
                name: '[name]_dll_[hash]',
            }),
        ],
    };
};

爲了方便DLL的管理,咱們還單獨開了個dll.js文件來管理webpack.dll.config.js的入口entry,咱們把全部須要DLLPlugin處理的庫都記錄在這個文件中:

const dll = {
    core: [
        'react',
        '@hot-loader/react-dom',
        'react-router-dom',
        'prop-types',
        'antd/lib/badge',
        'antd/lib/button',
        'antd/lib/checkbox',
        'antd/lib/col',
        ...省略
    ],
    tool: [
        'js-cookie',
        'crypto-js/md5',
        'ramda/src/curry',
        'ramda/src/equals',
    ],
    shim: [
        'whatwg-fetch',
        'ric-shim'
    ],
    widget: [
        'cecharts',
    ],
};

module.exports = {
    dll,
    dllNames: Object.keys(dll),
};

對於要把哪些庫放入DLL中,請根據本身項目的狀況來定,對於一些特別大的庫,又無法作模塊分割和不支持Tree Shaking的,好比Echarts,建議先去官網按項目所需功能定製一套,不要直接使用整個Echarts庫,不然會白白消耗許多的下載時間,JS預處理的時間也會增加,減弱首屏性能。

而後咱們在webpack.config.js的plugins配置中加入DLLReferencePlguin來對DLLPlugin處理的庫進行映射,好讓編譯後的代碼可以從window對象中找到它們所依賴的庫:

{
    ...省略

    plugins: [
        ...省略
        
        // 這裏的...用於延展開數組,由於咱們的DLL有多個,每一個單獨的DLL輸出都須要有一個DLLReferencePlguin與之對應,去獲取DLLPlugin輸出的manifest.json庫映射文件。
     // dev環境下暫不採用DLLPlugin優化。
...(isEnvProduction ? dllNames.map(dllName => new DllReferencePlugin({ context: path.resolve(__dirname), manifest: path.resolve(__dirname, '..', `build/static/dll/${dllName}.manifest.json`) })) : [] ), ...省略 ] ... }

咱們還須要在承載咱們應用的index.html模板中加入<script>,從webpack.dll.config.js裏配置的output輸出文件夾中前置引用這些DLL庫。對於這個工做DLLPlguin和它的搭檔不會幫咱們作這件事情,而已有的html-webpack-plugin也不能幫助咱們去作這件事情,由於咱們無法經過它往index.html模板加入特定內容,但它有個加強版的兄弟script-ext-html-webpack-plugin能夠幫咱們作這件事情,筆者以前也用過這個插件內聯JS到index.html中。但筆者懶得再往node_modules中加依賴包了,另闢了一個蹊徑:

CRA這套架子已經使用了DefinePlugin來在編譯時建立全局變量,最經常使用的就是建立process環境變量,讓咱們的代碼能夠分辨是開發仍是生產環境,既然已有這樣的設計,何不繼續使用,讓DLLPlugn編譯的獨立JS文件名暴露在某個全局變量下,並在index.html模板中循環這個變量數組,循環建立<script>標籤不就好了,在上面提到的dll.js文件中最後導出的 dllNames 就是這個數組。

而後咱們改造一下index.html:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <% if (process.env.NODE_ENV === "production") { %>
            <% process.env.DLL_NAMES.forEach(function (dllName){ %>
                <script src="/static/dll/<%= dllName %>.dll.js"></script>
            <% }) %>
        <% } %>
    </head>
    <body>
        <noscript>Please allow your browser to run JavaScript scripts.</noscript>
        <div id="root"></div>
    </body>
</html>

最後咱們改造一下build.js腳本,加入打包DLL的步驟:

function buildDll (previousFileSizes){
    let allDllExisted = dllNames.every(dllName =>
        fs.existsSync(path.resolve(paths.appBuildDll, `${dllName}.dll.js`)) &&
        fs.existsSync(path.resolve(paths.appBuildDll, `${dllName}.manifest.json`))
    );
    if (allDllExisted){
        console.log(chalk.cyan('Dll is already existed, will run production build directly...\n'));
        return Promise.resolve();
    } else {
        console.log(chalk.cyan('Dll missing or incomplete, first starts compiling dll...\n'));
        const dllCompiler = webpack(dllConfig);
        return new Promise((resolve, reject) => {
            dllCompiler.run((err, stats) => {
                ...省略
            })
        });
    }
}

 

checkBrowsers(paths.appPath, isInteractive)
    .then(() => {
        // Start dll webpack build.
        return buildDll();
    })
    .then(() => {
        // First, read the current file sizes in build directory.
        // This lets us display how much they changed later.
        return measureFileSizesBeforeBuild(paths.appBuild);
    })
    .then(previousFileSizes => {
        // Remove folders contains hash files, but leave static/dll folder.
        fs.emptyDirSync(paths.appBuildCSS);
        fs.emptyDirSync(paths.appBuildJS);
        fs.emptyDirSync(paths.appBuildMedia);
        // Merge with the public folder
        copyPublicFolder();
        // Start the primary webpack build
        return build(previousFileSizes);
    })
    .then(({stats, previousFileSizes, warnings}) => {
        ... 省略
    })
    ... 省略

大體邏輯就是若是xxx.dll.js文件存在且對應的xxx.manifest.json也存在,那麼就不從新編譯DLL,若是缺失任意一個,就從新編譯。DLL的編譯過程走完後再進行主包的編譯。因爲咱們將DLLPlugin編譯出的文件也放入build文件夾中,因此以前每次開始編譯主包都須要清空整個build文件夾的操做也修改成了僅僅清空除了放置有dll.js和manifest.json的目錄。

若是咱們的底層依賴庫確實發生了變化,須要咱們更新DLL,按照以前的檢測邏輯,咱們只須要刪除整個某個dll.js文件便可,或者直接刪除掉整個build文件夾。

哈哈,到此全部的有關DLL的配置就完成了,大功告成。

本段落開始時有提到過DLLPlugin的使用都是在生產環境下。由於開發環境下的打包狀況很特殊並且複雜:

在開發環境下整個應用是經過webpack-dev-server來打包並起一個Express服務來serve的。Express服務在內部掛載了webpack-dev-middleware做爲中間件,webpack-dev-middleware能夠實現serve由Webpack compiler編譯出的全部資源、將全部的資源打入內存文件系統中的功能,並能結合dev-sever實現監聽源文件改動並提供HRM。webpack-dev-server接收了一個Webpack compiler和一個有關HTTP配置的config做爲實例化時的參數。這個compiler會在webpack-dev-middleware中執行,用監聽方式啓動compiler後,compiler的outputFileSystem會被webpack-dev-middleware替換成內存文件系統,其執行後打包出來的東西都沒有實際落盤,而是存放在了這個內存文件系統中,而ouputFileSystem自己是在Node.js的fs模塊基礎上封裝的。將編譯結果存放進內存中是webpack-dev-middleware內部最奇妙的地方。事實上就算將文件資源落盤,也必須先把文件從磁盤讀到內存中,再以流的形式返回給客戶端,這樣一來會多一個從磁盤中將文件讀進內存的步驟,反而尚未直接操做內存快。內存文件系統在npm start命令起的進程被幹掉後,就被回收了,下一次再起進程的時候,會建立一個全新的內存文件系統。

這裏順帶再說下webppack-dev-middle源碼的流程:

首先在執行wdm這個入口文件導出的方法中,經過setFs方法將本來的Node.js的fs給替換爲memory-fs內存文件系統,因此下面提到的文件系統都是指這個內存文件系統。

按照Express的流程,全部請求都會依次進入Express掛載的中間件中,在webppack-dev-middle中間件中,會先經過getFilenameFromUrl方法根據傳入的此次請求的url,compiler和webpack的publicPath等有關配置,肯定一個finame的URI,這個URI最終會返回三種結果,這三種結果將走三種不一樣的處理流程:
結果1:一個布爾值false
結果2:一個HTTP API的請求路徑
結果3:一個靜態文件資源的請求路徑

結果1的後續處理:表示請求url帶有hostname等特徵,也就肯定這個發給dev-server的請求並非須要webpack-dev-middle處理的,將執行goNext(),根據是不是serverSideRender,在goNext方法中的處理過程會有所不一樣,但最終都會調用Express應用的next()進入下一個中間件。


結果2的後續處理:經過文件系統的statSync方法會判斷這個路徑並非一個文件資源的路徑,再根據isDirectory方法判斷它是不是一個目錄的路徑,若是是的話,再看看該目錄下是否有一個index.html的文件,有就返回。在開發的時候咱們在瀏覽器地址欄輸入"localhost:3000"就是走的這個流程,會返回承載前端應用的那個入口index.html文件。


結果3的後續處理:經過文件系統的statSync方法會判斷出這個路徑就是一個文件資源的路徑,會經過文件系統的readFileSync方法從內存中讀取到該文件內容,再拿到該資源對應的MIME類型,設置好response的header返回給瀏覽器,瀏覽器就能正確解析返回的文件資源流了。咱們在開發環境下,全部對js、js.map、圖片資源等除了index.html外的請求都是走的這個流程。

全部,因爲開發環境打包的特殊性,在開發環境下將編譯DLL的compiler的和主compiler結合起來還須要再看看,所以怎麼在開發環境使用DLLPlugin還須要再研究下。因此筆者只是在開發環境使用了多線程和各類緩存。因爲開發環境下,編譯的工做量少於生產環境,而且對全部資源的讀寫都是走內存的,所以速度很快,每次檢測到源文件變更並進行重編譯的過程也會很快。因此開發環境的編譯速度在目前來看還能夠接受,暫時不須要優化。

這裏順帶說一句,筆者以前在看有關DLLPlugin的文檔和文章時,也注意到了一個現象,就是不少人都說DLLPlugin過期了,而externals使用起來更方便,配置更簡單,甚至在CRA、vue-cli這些最佳實踐的腳手架中都已經沒再繼續使用DLLPlugin了,由於Webpack4.x的編譯速度已經足夠快了。筆者的體會就是:我這個項目就是基於Webpack4.X的,項目規模變大之後,沒有以爲4.X有多麼地快。筆者的項目在交付客戶後也極大可能不能訪問互聯網,因此經過配置externals將資源請求導向CDN的方式對筆者的項目來講沒有用,只能經過使用DLLPlugin提升生產包的編譯速度。我想這也是爲何Webpack到了4.X版本依然沒有去掉DLLPlugin的緣由,由於不是全部的前端項目都必定是互聯網項目,實踐出真知。

 

6、編譯速度提高了多少?

筆者開發機4和8線程,單核基礎頻率2.2GHz。全部測試都基於已對Echarts進行功能定製。

1. 最原始的CRA腳手架編譯筆者這個項目的速度。

初次編譯,無任何CRA原始配置的緩存,這和最初在CI上進行編譯的狀況徹底同樣,每次差很少都是這個耗時。

由於每次node_moduels都要刪除重來,沒法緩存任何以前的編譯結果:

大概1分10多秒。CI上的話,大概會稍微快點,由於硬件性能好些。

有緩存之後:

若是不定製Echarts的話,直接引入整個Echarts,在沒緩存的時候大概會多5秒耗時。

 

2. 引入dll後。

初次打包,而且無任何DLL文件和CRA原始配置的緩存:

先編譯DLL,再編譯主包,整個過程耗時直接變成了57秒。

有DLL文件和緩存後:

降到27秒多了。

 

3.最後咱們把多線程和cache-loader上了:

無任何DLL文件、CRA原始配置的緩存以及cache-loader的緩存時:

大概在接近60秒左右。

有DLL文件和全部緩存後:

最終,耗時已經降低至17秒左右了。在運行CI的編譯服務器上執行的話,速度還會更快些。

在打包速度上,比最原始的1分10多秒耗時已經有本質上的提高了。這纔是咱們想要的。

 

7、總結

至此,咱們已經將全部的底層依賴DLL化了,把幾乎全部能緩存的東西都緩存了,並支持多線程(多進程模擬)編譯。當項目穩定後,不管項目規模多大,小幅度的修改將始終保持在這個編譯速度,耗時不會有太大的變化。

聽說在Webpack5.X帶來了全新的編譯性能體驗,很期待使用它的時候。談及到此,筆者只以爲有淡淡的憂傷,那就是前端技術、工具鏈、框架、開發理念這些的更新速度實在是太快了。就拿Webpack這套構建平臺來講,當Webpack5.X普及後,Webpack4.X這套優化可能也就過期了,整套工具鏈又須要從新學習。

相關文章
相關標籤/搜索