(中篇)梳理 Webpack5优化配置

前言

作为一个前端工程师,前端工程化是必过的坎,而Webpack在前端工程化中扮演着至关重要的角色。 要明白webpack为什么重要,就要知道它为什么出现,解决了什么问题。

前端模块化从文件划分模块、命名空间划分模块、IIFE通过约定来实现模块化,到CommonJS、ES Module通过行业规范实现模块化,如今ES Module已经是最正统最主流的模块化规范,但是它依然还存在兼容问题,所以开发者还需要解决兼容问题。而且模块化开发,会划分出很多文件,每个文件就是一个模块,文件过大,浏览器请求、加载文件时间过长,影响页面渲染速度,文件过多浏览器请求频繁,也影响性能,所以需要对这些文件进行合并拆分。而项目复杂后,html、css、图片、字体文件等也需要模块化来管理。

于是webpack顺势而出,它是一个现代化的模块打包工具,支持js、css等不同种类资源的模块化(项目中使用的每个文件都是一个模块),同时对这些资源做兼容性处理,最后对这些资源文件根据需要做拆分合并压缩后打包为静态资源

目前,webpack已经到了 webpack 5.89.0 了,应该静下心来好好看一看官网上篇文章梳理了webpack5的基础配置,并用webpack5从零搭建一个vue3+ts项目,这篇文章梳理webpack5的优化配置,继续在上一篇文章的配置基础上修改。

首先要做的一步,当然是尽可能使用最新的webpack。

webpack文档中有专门的一块内容讲构建性能

查看打包时间和打包体积

安装 speed-measure-webpack-pluginpnpm add -D speed-measure-webpack-plugin,但是这个插件不兼容一些新版插件,比如mini-css-extract-plugin,打包会报错,要用只能将不兼容的插件降版本。

安装 webpack-bundle-analyzer 分析打包体积

js 复制代码
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [new BundleAnalyzerPlugin()]
}

一、优化构建速度

1、提高模块解析速度

resolve.alias

当在项目里面使用 importrequire相对路径,引入的资源文件所处的目录,被认为是上下文目录。在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径。 配置别名alias,确保模块引入变得更简单,减少webpack的路径拼接查找时间, 从

js 复制代码
import Utility from '../../utilities/utility';

改成

js 复制代码
const path = require('path');
module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
    },
  },
};
js 复制代码
import Utility from 'Utilities/utility';

resolve.extensions

配置 resolve.extensions,webpack 会按顺序解析列在数组首位的后缀的文件并跳过其余的后缀,将使用频率高的文件放前面,可以加快webpack解析速度;

js 复制代码
    resolve: {
      extensions: [".vue", ".ts", ".scss", "..."],
    },

最好的实践,还是尽量带上后缀,减少匹配过程,加快解析速度

resolve.mainFields

一般我们在项目里面都是用的配置的别名resolve.alias,绝对路径、或者相对路径,但是引入npm包使用模块路径, 当我们从npm包中导入模块时,import 'xxx' from 'yyy',根据 模块解析规则,包里面的package.json里面一般都是用main、module、jsnext:main、browser、exports字段指定入口文件,webpack里面关于优先级的说明:

exports field is preferred over other package entry fields like main, module, browser or custom ones.

如果不能通过npm包的package.json里面的exports字段直接找到入口文件,或者没有exports字段 可以修改一下 resolve.mainFields,web端默认 ['browser', 'module', 'main'],node端默认['module', 'main'] ,根据情况,看项目使用esm还是cjs模块化方案,调整mainFields的顺序,加快查找速度

js 复制代码
module.exports = {
  //...
  resolve: {
    mainFields: ['main','module', 'browser'],
  },
};

resolve.modules

如果在项目中使用较多模块路径,比如import math.js from 'utils',可以配置 resolve.modules,它默认是['node_modules'],webpack 查找当前目录以及祖先路径(即 ./node_modules, ../node_modules 等等)

如果有很多模块路径来自src,可以优先搜索src,

js 复制代码
module.exports = {
  //...
  resolve: {
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
  },
};

2、不需要解析的模块 module.noParse

项目中如果用了类似jquery或者loadsh这种库,没有使用AMD/CommonJs规范,没有模块化,就可以使用noparse排除解析,因为它们两个没有其他依赖,

js 复制代码
module.exports = {
  //...
  module: {
    noParse: /jquery|lodash/,
  },
};

排除的文件里面不应该含有 import, require, define 的调用,或任何其他导入机制,打包后浏览器不兼容,就报错; 比如我安装了loadsh-es,它里面是使用了import引入其他文件,如果不解析它,就会报错

3、排除多余模块 IgnorePlugin

有些npm包,模块被捆绑在了一起,比如webpack官网的例子,moment这个包,我只需要zh-cn这个模块,不需要其他国家的语言模块。

js 复制代码
new webpack.IgnorePlugin({
  resourceRegExp: /^./locale$/,
  contextRegExp: /moment$/,
});

4、减少loader处理文件

使用loader的时候,使用include和exclude缩小查询范围,exclude优先级更高,使用include包含更精确范围

js 复制代码
const resolve = (dir: string) => path.resolve(__dirname, dir);

  {         
         test: /\.vue$/,
          include: resolve("./src"),
          loader: "vue-loader"
      },

5、使用多进程构建

多余耗时的loader,像babel-loader可以使用thread-loader每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。

请仅在耗时的操作中使用此 loader!

js 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve('src'),
        use: [
          "thread-loader",
          "babel-loader" // 耗时的 loader (例如 babel-loader)
        ],
      },
    ],
  },
};

可以通过预警 worker 池来防止启动 worker 时的高延时。

这会启动池内最大数量的 worker 并把指定的模块加载到 node.js 的模块缓存中。

js 复制代码
const threadLoader = require('thread-loader');

threadLoader.warmup(
  {
    // 池选项,例如传递给 loader 选项
    // 必须匹配 loader 选项才能启动正确的池
    workers: 2,
    // 一个 worker 进程中并行执行工作的数量
    workerParallelJobs: 50,
    // 闲置时定时删除 worker 进程
    poolTimeout: 2000 //(默认500ms),
  },
  [
    // 加载模块
    // 可以是任意模块,例如
    'babel-loader',
    'babel-preset-es2015',
    'sass-loader',
  ]
);

6、缓存

在 webpack 配置中使用 cache 选项实现持久化缓存。 缓存生成的 webpack 模块和 chunk,来改善构建速度。cache 会在开发模式被设置成 type: 'memory'内存缓存, 而且在生产模中没有默认开启。 cache: truecache: { type: 'memory' } 配置作用一致

js 复制代码
module.exports = {
  cache: {
    type: "filesystem",
    buildDependencies: {
      // This makes all dependencies of this file - build dependencies
      config: [__filename],
    },
  },
};

二、优化开发体验

1、source map

浏览器运行的代码通常是压缩转换过的,这样可以节约文件下载时间,但是debug就困难了,出现问题,不好准确定位到报错行,source map就是一个将压缩转换后的代码映射到原始代码的文件,它能够让浏览器重新构建原始代码文件,并把情况展示在console面板这样的调试器中。

要做到这一点:

  • 生成source map文件
  • 并且在转换后的代码文件底部添加特殊注释指向source map文件,注释的语法格式:
js 复制代码
//# sourceMappingURL=main.c8400759.js.map

同时在chrome浏览器中,(已经默认勾选),必须将 devtool->sources->settings->preference->ennable javascript source maps勾选上,浏览器才会下载source map文件进行解析映射。

使用webpack的devtool选项控制是否生成,以及如何生成 source map。

对于开发环境以下选项非常适合:

  • eval - 每个模块都使用 eval() 执行,并且都有 //# sourceURL。此选项会非常快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码(没有从 loader 中获取 source map),所以不能正确的显示行数。
  • eval-source-map - 每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。
  • eval-cheap-source-map - 类似 eval-source-map,每个模块使用 eval() 执行。这是 "cheap(低开销)" 的 source map,因为它没有生成列映射(column mapping),只是映射行数。它会忽略源自 loader 的 source map,并且仅显示转译后的代码,就像 eval devtool。
  • eval-cheap-module-source-map - 类似 eval-cheap-source-map,并且,在这种情况下,源自 loader 的 source map 会得到更好的处理结果。然而,loader source map 会被简化为每行一个映射(mapping)。

在大多数情况下,最佳选择是 eval-cheap-module-source-map

三、优化构建产物

1、代码分割

代码分割是 webpack 中最主要的特性之一。此特性能够把代码分离到不同的 bundle 中,然后便能按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle、控制资源加载优先级,如果使用合理,会极大减小加载时间,提升页面加载速度。

常用的代码分离方法有三种:

  • 多个入口 :使用 entry 配置手动地分离代码。
  • 防止重复 :使用 入口依赖 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用分离代码。

(1)多个入口

js 复制代码
module.exports = {
  //...
  entry: {
    index: './src/index.ts',
    home: './src/home.ts',    
  },
};

此时output.filename不能写死,不然会报错,多个入口,无法对应出口文件,可以写成以下方式:

js 复制代码
module.exports = {
  //...
  output: {
    // 使用入口名称
    filename: '[name].bundle.js',
    // 使用内部 chunk id
    // filename: '[id].bundle.js',
    // 使用由生成的内容产生的 hash
    // filename: '[contenthash].bundle.js'
    // 结合多个替换组合使用
    // filename: '[name].[contenthash].bundle.js'
    // 使用函数返回 filename
    // filename: (pathData) => {
     // return pathData.chunk.name === 'main' ? 
     // '[name].js' : '[name]/[name].js';
    },
  },
};

但是这种方式有隐患:如果index.ts和home.ts都引入了lodash-es这个库,

js 复制代码
// index.ts
import { join } from "lodash-es";
console.log(join(["Another", "module", "loaded!"], " "));

// home.ts
import { join } from "lodash-es";
console.log(join(["Another", "module", "loaded!"], " "));

join函数会重复打包,在配置文件中配置 dependOn 选项,以在多个 chunk 之间共享join模块。同时设置 optimization.runtimeChunk : 'single' ,创建一个在所有生成 chunk 之间共享的运行时文件,join模块只能实例化一次。这种保证允许模块的顶级作用域用于全局状态,并在join模块的所有使用者之间共享

js 复制代码
module.exports = {
  entry: {
    index: {
      import: "./src/index.ts",
      dependOn: "shared"
    },
    home: {
      import: "./src/home.ts",
      dependOn: "shared"
    },
    shared: ["lodash-es"]
  },
  output: {
    filename: "[name].[contenthash:8].js"
  },
  optimization: {
    runtimeChunk: "single"
  }
};

(2)import() 动态导入

js 复制代码
// index.ts
const btnEl = document.createElement('button')
btnEl.textContent = 'import导入'
btnEl.onclick=function(){
  import("./home").then((res) => {
    res.sendName();
  });
}
document.body.appendChild(btnEl)

// home.ts
export const sendName = () => {
  console.log("dynamic import");
};

home.ts会被单独打包成一个bundle文件,点击按钮的时候,才会创建一个script标签去请求它,做到按需加载

在使用vue-router或者react-router路由懒加载时,就是这个原理,依赖于打包工具的切割

(3)optimization.splitChunks

webpack v4+ 开始提供的全新的通用分块策略,配置 optimization.splitChunks,默认只对按需加载的chunk分包,比如import(),如果想要分更多包,就要配置optimization.splitChunks

默认配置:

js 复制代码
module.exports = {
  //...
  optimization: {
    splitChunks: {
      // 默认只将异步import()引入代码分割,
      chunks: "async",
      // 最小20kb才会分割
      minSize: 20000,
      // 除了满足minsize,还要减少主chunk的大小才会分割
      minRemainingSize: 0,
      // 拆分前必须共享模块的最小chunks数
      minChunks: 1,
      // 按需加载时的最大并行请求数
      maxAsyncRequests: 30,
      // 入口文件的最大并行请求数
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      // 缓存组可以继承和/或覆盖来自 splitChunks.* 的任何选项
      // test、priority 和 reuseExistingChunk 只能在缓存组级别上进行配置。
      // 可以配置多个组,如果一个模块满足多个组条件,最终由priority决定打包到哪个组
      cacheGroups: {
        // 默认将所有来自node_modules目录的模块打包至vendors组
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,
          // 则它将被重用,而不是生成新的模块。这可能会影响 chunk 的结果文件名。
          reuseExistingChunk: true
        },
        // 两个以上的chunk所共享的模块打包至default组
        default: {
          minChunks: 2,
          // 优先级没有defaultVendors高
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

根据项目不同情况配置

js 复制代码
  optimization: {
    usedExports: true,
    splitChunks: {
      cacheGroups: {
        // 包括整个应用程序中 node_modules 的所有代码。
        vendor: {
          name: "vendor",
          chunks: "all",
          priority: 20,
          test: /[\\/]node_modules[\\/]/
        },
        // 包括入口(entry points)之间所有共享的代码
        commons: {
          name: "commons",
          chunks: "initial",
          priority: 10,
          minSize: 0,
          // 至少被两个chunk共享才分离
          minChunks: 2
        }
      }
    }
  }

(4)预获取和预加载

ts 复制代码
// home.ts
export const sendName = () => {
  console.log("dynamic import");
};

// index.ts
const btnEl = document.createElement("button");
btnEl.textContent = "import导入";
btnEl.onclick = function () {
  import(/* webpackPrefetch: true */ "./home").then((res) => {
    res.sendName();
  });
};
document.body.appendChild(btnEl);

上面的代码在构建时会生成 <link rel="prefetch" href="./home.ts"> 并追加到页面头部,指示浏览器在闲置时间预获取 home.ts 文件,不要等点击按钮再获取,优化用户体验。

js 复制代码
import(/* webpackPreload: true */ "./home").then((res) => {
    res.sendName();
  });

preload具有更高优先级,home.ts会和index.ts并行下载,但不会解析执行

2、tree-shaking

sideEffectsusedExports(更多地被称为 tree shaking)是两种不同的优化方式

sideEffects 更为有效 是因为它允许跳过整个模块/文件和整个文件子树。

usedExports 依赖于 terser 检测语句中的副作用。它是一个 JavaScript 任务而且不像 sideEffects 一样简单直接。并且由于规范认为副作用需要被评估,因此它不能跳过子树/依赖项。

一种方式是在package.json里面配置sideEffects字段:

所有代码都没有副作用,直接设置false,

json 复制代码
"sideEffects": false

但是这样做,在首页引入的 css文件也不会被打包,换成数组方式

json 复制代码
  "sideEffects": [
    "*.css"
  ]

还有一种方式,在生产模式下打包,webpack会自动tree-shaking,相当于配置

js 复制代码
module.exports = {
  // ...
  optimization: {
    usedExports: true,
  },
}

同时可以使用/*#__PURE__*/ 注释放到函数调用之前,用来标记此函数调用是无副作用的。

要对css做tree-shaking,安装插件purgecss-webpack-plugin

js 复制代码
const path = require("path");
const { globSync } = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgeCSSPlugin = require("purgecss-webpack-plugin");

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.s?css$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "postcss-loader",
          "sass-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "style/[name]_[contenthash:8].css",
      chunkFilename: "style/[name]_[chunkhash:8].css"
    }),
    new PurgeCSSPlugin({
      paths: globSync(`${path.resolve(__dirname, "src")}/**/*`, { nodir: true })
    })
  ]
};

3、代码压缩

webpack5 在生产环境下已经使用 terser-webpack-plugin 自动对js代码做了压缩,如果要自定义配置,那么仍需要安装 terser-webpack-plugin

同时也发现在生产模式下css代码也是被压缩了的,不用在手动的配置 css-minimizer-webpack-plugin,而且被注释的css代码也被删除了

总结

Webpack在前端工程化中提供的能力:

  1. 模块打包和依赖管理:Webpack可以将前端应用程序拆分为多个模块,并通过各种加载器(Loaders)和插件(Plugins)来处理和转换这些模块。它可以解析模块之间的依赖关系,并生成一个或多个打包后的文件,以供浏览器加载和执行。
  2. 资源管理和优化:Webpack不仅可以打包JavaScript模块,还可以处理其他类型的静态资源,如样式表(CSS、Sass、Less)、图片、字体等。通过加载器和插件,它可以对这些资源进行压缩、合并、优化和缓存等处理,以提高应用程序的加载性能和用户体验。
  3. 代码分割和懒加载:Webpack支持代码分割和懒加载,可以将应用程序代码拆分为多个块(chunks),并按需加载这些块。这种方式可以减小初始加载的文件大小,提高页面的加载速度,并实现按需加载,降低了用户首次访问时的等待时间。
  4. 开发环境和生产环境的配置:Webpack提供了强大的配置能力,可以根据开发环境和生产环境的需求来进行不同的配置。它支持开发服务器、热模块替换(Hot Module Replacement)、代码调试等功能,使开发人员能够更高效地进行开发和调试。
  5. 构建流程的自动化:Webpack可以与其他构建工具(如Grunt、Gulp)集成,并通过配置文件定义整个构建流程。它可以自动化处理资源依赖关系、编译、压缩、合并和输出最终的生产代码,简化了前端开发的构建过程,提高了开发效率。

兄弟篇

(上篇)梳理Webpack5基础配置、从0搭建一个Vue3+Ts项目(字数8k+)

相关推荐
WeiXiao_Hyy3 小时前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡4 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone4 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09014 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农4 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king5 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳5 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵6 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星6 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_6 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js