webpack5优化性能配置

504 阅读5分钟

在vscode中配置webpack的时候,如果想要代码提示,可以在配置对象上添加一个注释,这样就可以有较好的提示了。

/** @type {import('webpack').Configuration} */
module.exports = {
...
}

分析插件

费时分析

查看每个阶段和插件的费时

    const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
    const smw = new SpeedMeasureWebpackPlugin();
    module.exports =smw.wrap({
    });

image.png

webpack-bundle-analyzer

启动一个服务端口,查看生成代码分析报告,帮助提升代码质量和网站性能

    const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
    module.exports={
        plugins: [
            new BundleAnalyzerPlugin()
        ]
    }

细节配置

entry output

入出口配置,要注意output中的path要用绝对路径

let path = require('path')
module.export = {
    // 此处配置的是trunk,属性名为trunk名称,属性值为入口模块
    entry:{
        // 默认trunk名为main
        main:'./src/index.js'
        // 启动模块有两个,两个文件的内容会被合并到一个js文件中
        a:'["./src/a.js","./src/b.js"]'
    },
    output:{
        // 输出路径,注意必须是绝对路径,表示资源放置的位置
        path:'path.resolve(__dirname,src/dist)',
        // 合并后的js代码文件,配置的是js合文件的规则,取5位的chunkhash
        //filename可以深层选择如 filename:"scripts/bundle.js"就会加入scripts这个文件夹
        filename:"[name],[chunkhash:5].js"
    }
}

context

    context: path.resolve(__dirname, "app")

该配置会影响入口和loaders的解析,入口和loaders的相对路径会以context的配置作为基准路径,这样,你的配置会独立于CWD(current working directory 当前执行路径)

exclude/include

在配置loader的时候,可以通过配置exlude,include,祛除某些不需要loader转换的代码,提升打包速度。

//webpack.config.js
const path = require('path');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js[x]?$/,
                use: ['babel-loader'],
                include: [path.resolve(__dirname, 'src')]
            }
        ]
    },
}

extensions

指定extension之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配

resolve: {
  extensions: [".js",".jsx",".json",".css"]
},

alias

  • 配置别名可以加快webpack查找模块的速度,每当引入bootstrap模块的时候,它会直接引入bootstrap,而不需要从node_modules文件夹中按模块的查找规则查找
  • 在引入本地组件的时候可以简写路径
    const bootstrap = path.resolve(__dirname,'node_modules/bootstrap/dist/css/bootstrap.css')
    resolve: {
        alias:{
            bootstrap,
            '@': path.resolve(__dirname, 'src')
        }
    },

modules

  • 对于直接声明依赖名的模块(如 react ),webpack 会类似 Node.js 一样进行路径搜索,搜索node_modules目录
  • 这个目录就是使用resolve.modules字段进行配置的 默认配置
    resolve: {
        modules: ['node_modules'],
    }

resolveLoader

resolve.resolveLoader用于配置解析 loader 时的 resolve 配置,默认的配置:

module.exports = {
  resolveLoader: {
    modules: [ 'node_modules' ],
    extensions: [ '.js', '.json' ],
    mainFields: [ 'loader', 'main' ]
  }
};

mainFields

默认情况下package.json 文件则按照文件中 main 字段的文件名来查找文件

    resolve: {
        // 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
        mainFields: ['browser', 'module', 'main'],
        // target 的值为其他时,mainFields 默认值为:
        mainFields: ["module", "main"],
    }

mainFiles

当目录下没有 package.json 文件时,我们说会默认使用目录下的 index.js 这个文件,其实这个也是可以配置的

    resolve: {
        mainFiles: ['index'], // 你可以添加其他默认使用的文件名
    },

libraryTarget 和 library

当用 Webpack 去构建一个可以被其他模块导入使用的库时需要用到它们

  • output.library配置导出库的名称
  • output.libraryExport配置要导出的模块中哪些子模块需要被导出。 它只有在 output.libraryTarget 被设置成 commonjs 或者 commonjs2 时使用才有意义
  • output.libraryTarget配置以何种方式导出库,是字符串的枚举类型:var(默认) commonjs commonjs2 this window umd

性能优化

noParse

  • module.noParse 字段,可以用于配置哪些模块文件的内容不需要进行解析
  • 不需要解析依赖(即无依赖) 的第三方大型类库(jquery、lodash),可以通过这个字段来配置,以提高整体的构建速度
    module: {

        // 正则表达式
        noParse: /jquery|lodash/, 

        // 或者使用函数
        noParse(content) {
            return /jquery|lodash/.test(content)
        },
    }

IgnorePlugin

IgnorePlugin用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去

  • src/index.js
    import moment from  'moment';
    import 'moment/locale/zh-cn'
    console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
  • webpack.config.js
    new webpack.IgnorePlugin({

        //A RegExp to test the context (directory) against.
        contextRegExp: /moment$/,

        //A RegExp to test the request against.
        resourceRegExp: /^\.\/locale/
    });

thread-loader

thread-loader 放置在其它 loader 之前,那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。 在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能产生新的文件。
  • 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
  • 这些 loader 无法获取 webpack 的选项设置。

externals

我们可以将一些JS文件存储在 CDN 上(减少 Webpack打包出来的 js 体积),在 index.html 中通过 <script> 标签引入,如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="root">root</div>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
</html>

我们希望在使用时,仍然可以通过 import 的方式去引用(如 import $ from 'jquery'),并且希望 webpack 不会对其进行打包,此时就可以配置 externals

// webpack.config.js
module.exports = {
    //...
    externals: {
        //jquery通过script引入之后,全局中即有了$变量
        'jquery': '$'
    }
}

这样配置以后,当在业务代码中引用jquery的时候,打包后的结果模块会将jquery模块这样导出,因为已经通过cdn引入的代码将赋值为jquery实例,所以赋值为`jquery`实例,所以``可以直接用了。

let __webpack_modules__ = {
    'jquery': (module) => {
      module.exports = $;
  }
};

DllPlugin

DllPluginDllReferencePlugin主要功能可以将第三方包单独打包,有些时候,如果同一个包被多个chunk使用,会造成每个chunk文件都包含这个公用包的代码,导致chunk文件过大,这时就需要拆包了

  • DllPluginDLLReferencePlugin 可以实现拆分 bundles,并且可以大大提升构建速度,DllPluginDLLReferencePlugin 都是 webpack 的内置模块。

  • 我们使用 DllPlugin 将不会频繁更新的库进行编译,当这些依赖的版本没有变化时,就不需要重新编译。我们新建一个 webpack 的配置文件,来专门用于编译动态链接库,例如名为: webpack.config.dll.js,这里我们将 lodashjuqery 单独打包成一个动态链接库,通过entry配置。

  • DllPlugin 会生成两个文件,一个是动态链接库的json映射文件,一个要分出去的包,用于将需要分出去的包分出去,会暴露出一个全局变量通过DllPlugin options.name配置。

dll.config.js配置

const path = require('path');
const { DllPlugin } = require('webpack');

/** @type {import('webpack').Configuration} */
module.exports = {
    mode: 'development',
    devtool: false,
    entry: {
        utils: ['jquery', 'lodash']
    },
    output: {
        path: path.resolve(__dirname, 'dist/dll'),
        filename: 'utils.dll.js',
        library: 'dll_util'
    },

    plugins: [
        new DllPlugin({
            path: path.join(__dirname, 'dist/dll/manifest.json')
        })
    ]
};

manifest.json

{
    "name": "dll_util",
    "content": {
        "./node_modules/jquery/dist/jquery.js": {
            "id": "./node_modules/jquery/dist/jquery.js",
            "buildMeta": {}
        },
        "./node_modules/lodash/lodash.js": {
            "id": "./node_modules/lodash/lodash.js",
            "buildMeta": {}
        }
    }
}

utils.dll.js

var dll_util;
(()=>{
    ...
    var __webpack_exports__ = __webpack_require__("?2e89");
    dll_util = __webpack_exports__;
})();

webpack.config.js

const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

/** @type {import('webpack').Configuration} */
module.exports = {
    entry: './src/index.js',
    mode: 'development',
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
        }),
        new HtmlWebpackPlugin({
            template: './public/index.html'
        })
    ]
};

html使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index</title>
    <script defer src="./dll/utils.dll.js"></script>
    <script defer src="main.js"></script></head>
<body>
    
</body>
</html>

splitChunk

splitChunk是另一个分包的方法,用于分割公共代码

// webpack.config.js
module.exports = {
       optimization: {
            splitChunks: {
                // all = async + initial:同步异步都要分割
                chunks: 'all',

                // 分割出去的代码最小体积 0为不限制
                minSize: 0,

                // 被几个代码块引用才会分割
                minChunks: 2,
                
                // 缓存组
                cacheGroups: {
                    defaultVendors: {
                        test: /[\\/]node_modules[\\/]/,//   [\]node_modules[\]
                        priority: 0

                    },
                    default: {
                        minChunks: 2,
                        priority: 1
                    }
                }
            }
        },
}

异步分包预加载

对于import('xxx'),这种异步引入的文件,也会单独分出一个文件,对于这种异步文件我们可以利用html的preload(并行加载,针对重要的优先级高的文件),和prefetch(优先级较低的文件,利用网络进程空闲时请求),利用<link href="需要预加载的js文件路径" rel="preload" as="script">实现。

请求异步模块

index.js打包前的代码(点击按钮才请求asyncChunk文件)

btn.onclick = function () {
    import('./asyncChunk').then(res => {
        console.log(res);
    });
};

打包后的html代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index</title>
    <script src="main.js"></script>
    <link href="src_asyncChunk_js.js" rel="preload" as="script">
    </link>
</head>

<body>
    <button id="btn">按钮</button>
</body>

</html>
  • 不进行预加载,不加载异步文件 image.png
  • preload异步文件优先级最高
<link href="src_asyncChunk_js.js" rel="preload" as="script">

image.png

  • prefetch异步文件优先级最低
<link href="src_asyncChunk_js.js" rel="prefetch" as="script">

image.png

PreloadWebpackPlugin

这个插件可以给异步代码块设置预加载或者懒加载 推荐@vue/preload-webpack-plugin这个插件

const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin');

/** @type {import('webpack').Configuration} */
module.exports = {
    entry: './src/index.js',
    mode: 'development',
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html'
        }),
        new PreloadWebpackPlugin({
            // preload or prefetch,这个位置的内容会填入link的rel属性
            rel: 'prefetch',
            // 只对异步代码块进行预加载
            include: 'asyncChunks'
        })
    ]
};

实现一个简易的PreloadWebpackPlugin插件

const HtmlWebpackPlugin = require('html-webpack-plugin');

class PreloadWebpackPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        compiler.hooks.compilation.tap(
            this.constructor.name,
            compilation => {
                //在产出的html标签生成前确定要生成哪些代码块的link
                HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(this.constructor.name,
                    (_, callback) => {
                        this.generateLinks(compilation);
                        callback();
                    });
                HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tap(this.constructor.name,
                    // html-webpack-plugin 的配置数据
                    (htmlPluginData) => {
                        if (this.resourceHints) {

                            // 将resourceHints的资源添加到配置中
                            htmlPluginData.assetTags.styles = [
                                ...htmlPluginData.assetTags.styles,
                                ...this.resourceHints
                            ];
                            return htmlPluginData;
                        }

                    });
            }
        );
    }
    generateLinks(compilation) {
        const { rel = 'preload', include } = this.options;
        //获取本次编译产出了哪些代码
        let chunks = compilation.chunks;

        //或者没有指定包含的代码块,或者传了asyncChunks只包含异步代码块

        if (!include || include === 'asyncChunks') {
            // 过滤chunks,拿到异步加载的chunks
            chunks = chunks.filter(chunk => {
                return !chunk.canBeInitial();
            });
        }

        // 拿到异步chunk文件名称:{ 'src_asyncChunk_js.js' }
        const allFiles = chunks.reduce((accumulated, chunk) => {
            // 给老的数组累加上当前代码块包含的文件
            return accumulated.concat(chunk.files);
        }, []);

        // 去重
        const uniqueFiles = new Set(allFiles);
        const links = [];

        for (const file of uniqueFiles) {
            const href = file;
            const attributes = { href, rel };

            links.push({
                tagName: 'link',
                attributes
            });
        }
        // 保存要处理的文件列表
        this.resourceHints = links;
    }
}
module.exports = PreloadWebpackPlugin;

vscode去除注释的插件

推荐分析打包文件时去除注释的的vscode插件:marketplace.visualstudio.com/items?itemN…