webpack調優總結

  • 前言

webpack的出現爲前端開發帶來翻天覆地的變化,無論你是用React,Vue還是Angular,webpack都是主流的構建工具。我們每天都跟它打交道,但卻很少主動去了解它,就像寫字樓裏的禮儀小姐姐,既熟悉又陌生。隨着項目複雜度的上升,打包構建的時間會越來越長。終於有一天,你發現npm run dev後,去泡個茶,上了個廁所,跟同事bb一輪後回到座位,項目還沒構建完的時候,你就會下定決心好好了解下這個熟悉的陌生人。

這次優化的目的主要有兩個:

  • 加快編譯構建速度
  • 減少頁面加載的時間

現狀是每次開發模式構建,大概要花120秒;生產模式構建,大概要花300秒。項目總共有將近150個chunk。

  • 加快編譯構建速度

有2種方式可以加快編譯的速度,分別是減少每次打包的文件數目,並行的去執行打包任務。這裏用到了2個webpack插件:

  • DllPlugin(減少每次打包的文件數目)
  • HappyPack(並行的去執行打包任務)

下面對這兩個插件作詳細的介紹。

  • DllPlugin

dll是Dynamic Link Library(動態鏈接庫)的縮寫,是Windows系統共享函數庫的一種方式。將一些比較少改變的庫和工具,比如React、React-DOM,事先獨立打包成一個chunk,以後每次構建的時候再直接導入,就不用每次都對這些文件打包了。這裏有2個分解動作:

  • 獨立打包dll
  • 導入dll

使用DllPlugin可以獨立打包dll,具體的配置如下:

const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

const env = process.env.NODE_ENV;

module.exports = {

    entry: {
        vendor: ['react', 'react-dom', 'react-router', 'redux', 'react-redux', 'redux-thunk'],
    },

    output: {
        filename: '[name]_dll_[chunkhash].js',
        path: path.resolve(__dirname, 'dll'),
        library: '_dll_[name]',
    },

    resolve: {
        mainFields: ['jsnext:main', 'browser', 'main'],
    },

    plugins: [
        new webpack.DllPlugin({
            name: '_dll_[name]',
            path: path.join(__dirname, 'dll', '[name].manifest.json'),
        }),
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify(env),
            },
        }),
        new UglifyJSPlugin({
            cache: true,
            parallel: true,
            exclude: [/node_modules/],
            uglifyOptions: {
                compress: {
                    warnings: false,
                    drop_console: true,
                    collapse_vars: true,
                    reduce_vars: true,
                },
                output: {
                    beautify: false,
                    comments: false,
                },
            },
        }),
    ],
};

DllPlugin網上有一些例子,但都不完美,體現在以下2點:

  • 沒有壓縮代碼
  • 沒有hash,當依賴更新時無法通知瀏覽器更新緩存

第1點比較好處理,加上DefinePlugin和UglifyJSPlugin就可以了。處理第2點的時候,除了在output加上chunkhash,在引入dll的時候需要做一些額外的操作,下文會講解。

這時在package.json加上一個命令,npm run dll一下就會生成一個類似這樣的文件:vendor_dll_be1f5270e490dcb25f.js

{
    ...
    "scripts": {
        "dll": "cross-env NODE_ENV=production webpack --config webpack.dll.js --progress"
    }
    ...
}

dll生成後,就要在構建的配置文件裏將其引入,這時候就用到DllReferencePlugin和AddAssetHtmlPlugin,配置如下

const fs = require('fs');
const path = require('path');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

const files = fs.readdirSync(path.resolve(__dirname, 'dll'));
const vendorFiles = files.filter(file => file.match(/vendor_dll_\w+.js/));
const vendorFile = vendorFiles[0];

module.exports = {
    ...
    plugins: [
        ...
        new webpack.DllReferencePlugin({
            manifest: require('./dll/vendor.manifest.json'),
        }),
        new AddAssetHtmlPlugin({
            filepath: path.resolve(__dirname, `dll/${vendorFile}`),
            includeSourcemap: false
        }),
        ...
    ],
};

DllReferencePlugin的作用是將打包好的dll文件傳入構建的代碼裏面,而AddAssetHtmlPlugin的作用是在生成的html文件中加入dll文件的script引用。網上的例子一般是將dll的文件名直接寫死的,但由於在上一步構建dll的時候加入了hash,所以要通過fs讀取真實的文件名,再注入到html中。

  • HappyPack

大家都知道webpack是運行在node環境中,而node是單線程的。webpack的打包過程是io密集和計算密集型的操作,如果能fork多個進程並行處理各個任務,將會有效的算短構建時間,HappyPack就能做到這點。下面是它的相關配置:

const HappyPack = require('happypack');
const os = require('os');

const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.js$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                use: [{
                    loader: 'happypack/loader?id=happyBabel',
                }],
            },
            {
                test: /\.css$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: ['happypack/loader?id=happyCss'],
                }),
            }
        ],
        ...
        plugins: [
            ...
            new HappyPack({
                id: 'happyBabel',
                loaders: [{
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true,
                        presets: ['react', 'es2015', 'stage-0'],
                        plugins: ['add-module-exports', 'transform-decorators-legacy'],
                    },
                }],
                threadPool: happyThreadPool,
                verbose: true,
            }),
            new HappyPack({
                id: 'happyCss',
                loaders: ['css-loader', 'postcss-loader'],
                threadPool: happyThreadPool,
                verbose: true,
            }),
        ],

其中happyThreadPool是根據cpu數量生成的共享進程池,防止過多的佔用系統資源。

  • 減少頁面加載時間

對於web應用來說,減少頁面加載時間一般有2種方法。一是充分利用瀏覽器緩存,減少網絡傳輸的時間。另外就是減少JS運行的時間,通過SSR等方式實現。利用webpack能有效的抽取出共享的資源,提高緩存的命中率。這裏用到的插件除了上文提到的DllPlugin外,還有CommonsChunkPlugin,相關配置如下:

module.exports = {

    entry: {
        vendor: ['zent','lodash']
        app: ['babel-polyfill', 'react-hot-loader/patch', './src/main.js']
    },
    ...
    plugins: [
        ...
        new webpack.optimize.CommonsChunkPlugin({
            names: ['vendor'],
            minChunks: Infinity,
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'app',
            minChunks: 3,
            children: true,
            async: 'chunk-vendor',
        }),
        new webpack.optimize.CommonsChunkPlugin({
            names: ['manifest'],
            minChunks: Infinity,
        }),
        new webpack.HashedModuleIdsPlugin(),
        new InlineManifestWebpackPlugin({
            name: 'webpackManifest',
        }),
        ...
    ],
};

插件的第一部分是將vendor構建一個獨立包;第二部分是抽取app入口文件code split之後所有子模塊的公共模塊,進一步減少子模塊的大小;第三部分將webpack的啓動代碼獨立打成一個manifest包,配合HashedModuleIdsPlugin可以保證vendor的hash不變。InlineManifestWebpackPlugin的作用是將manifest文件內聯到html模板中,減少一次網絡請求。

  • 總結

經過上述的優化之後,開發模式構建只需要60秒左右;生產模式構建只需要150秒左右,時間減少一半!緩存命中方面,可以做到基礎模塊(React等)和比較少變動的模塊(組件庫)分離出來,當組件庫更新的時候依然可以使用基礎模塊的緩存(通過dll實現)。

通過這次的優化,對webpack的理解加深了不少,取得了比較不錯的優化效果。另外也學習了loader和plugin的工作原理,有機會另寫一篇文章分享。

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