前端最常用的打包工具webpack(四)-性能优化

这是重新学习webpack的第四篇文章,之前我也写过一篇关于webpack的文章。问题是过去快半年了,我已经将webpack忘得差不多,回头一看那篇文章完全是一头雾水,也没有办法,那个时候我才刚刚开始写前端方面的博客,对于知识梳理、文字表达都有一些欠缺,啪啪啪一下扔出来几千字,全是重点,没有任何过渡性的语言,看个一会就没什么兴趣。

综上所述,本次决定给webpack写一个比较详细的系列文章,目的是如果我半年后又忘记了webpack的具体内容,再来翻阅文章,不会像这次一样一头雾水。

前景提要

webpack就目前来说还是有一定的学习必要的,因为像Vite这一类新的打包工具还存在着一些问题,而webpack经过这么多年的沉淀,一时半会还不会被Vite完全取代,不过由于Vite的秒级启动,在未来这方面的技术一定是主流,因为它会大大减少等待项目启动的时间。

其实对于其它语言来说,前端的编译速度已经非常快了,但是现在依然在朝着更快的编译速度前进。


webpack性能优化主要从打包构建速度、代码调试、代码运行的性能这几个方面进行入手。

1. HMR

模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。 主要是通过以下几种方式,来显著加快开发速度: 保留在完全重新加载页面时丢失的应用程序状态。 只更新变更内容,以节省宝贵的开发时间。

就是说在不使用HMR的情况下,每一次代码修改就会将整个项目进行重新打包,如果项目文件比较少倒是无妨,如果是100个文件,每次修改就都要对这100个文件进行重新打包,这样每次修改一下代码重新打包就要几秒钟甚至几十秒钟,也就是说你修改一句代码你要过几秒钟才能看到效果,这无疑大大减缓了开发效率,而HMR就是为了解决这一痛点。

开启了HMR功能后,webpack就会监听哪些文件发生了变化,一旦文件发生变化,就只会打包被修改的那个文件,其它文件都不会重新打包,这就大大加快了打包的效率。

devServer: {
  // 项目构建够的路径
  contentBase: resolve(__dirname, "build"),
  // 启动gzip压缩
  compress: true,
  // 端口号
  port: 3000,
  // 自动打开浏览器
  open: true,
  // HMR模式
  hot: true,
},

打开HMR模式十分简单,只需要设置hot: true

但是像css、html、js这类的文件默认不支持HMR,所以需要对这些文件进行一些配置,才能让各种类型的文件支持HMR模式。

1.1 css

在我们使用的style-loader中,默认支持HMR,因为HMR仅在开发环境需要,所以我们仅需要在开发环境使用style-loader就可以了。

1.2 html

默认不支持,需要进行多入口配置,将html文件也设置为入口就可以实现html的HMR。

entry: ["./main.js", "./public/index.html"],

1.3 js

js的热更新监听比较麻烦,需要在入口文件中进行配置。

并且每增加一个模块都需要进行配置。

// 是否开启了热更新
if (module.hot) {
  module.hot.accept("./test.js", function () {
    // 方法会监听test.js文件的变化,一旦发生变化,其它文件默认不会重新打包
    // 会执行该方法的函数
  });
}

js的HMR是无法对入口文件进行作用的,因为入口文件监听着其它文件的HMR如果入口文件一旦变化,其它文件都会重新进行打包。

2. source-map

一种提供源代码到构建后代码映射技术,即如果代码出现报错,是否会指向源代码中错误的那一行。

因为在打包后项目文件都经过压缩,可能已经变得和你的源代码完全不一样,如果发生了错误,你很难定位到你的源代码,就不知道你写的代码到底哪儿报错,所以我们就需要使用source-map将打包后的代码和源代码形成一个对应关系,可以让你找到你源代码中的错误。

使用方式只需要在webpack.config.js中新增一行代码:

devtool: "eval-source-map",

2.1 类型

source-map一共分为下面几种类型。

1、source-map:外部

  • 错误代码准确信息和源代码的错误位置

2、inline-source-map:内联

  • 只生成一个内联source-map
  • 错误代码准确信息和源代码的错误位置

3、hidden-source-map:外部

  • 错误代码错误原因,但是没有错误位置
  • 不能追踪源代码错误,只能提示到构建后代码的错误位置

4、eval-source-map:内联

  • 每一个文件都生成对应的source-map,都在eval错误代码准确信息和源代码的错误位置

5、nosources-source-map:外部

  • 错误代码准确信息,但是没有任何源代码信息

6、cheap-source-map:外部

  • 错误代码准确信息和源代码的错误位置只能精确到行

7、cheap-module-source-map:外部

  • 错误代码准确信息和源代码的错误位置

对于不同的环境,我们需要配置的devtool也不相同。

2.2 开发环境

在开发环境中,我们往往需求的是速度快,调试更友好

速度快(eval>inline>cheap>...

  • eval-cheap-source-map
  • eval-source-map

调试更友好

  • souce-map
  • cheap-module-source-map
  • cheap-source-map

所以推荐选择:

  • eval-source-map:调试最友好(React、Vue默认使用)。
  • eval-cheap-module-souce-map:性能更友好。

2.3 生产环境

在生成环境中,我们需要考虑源代码要不要隐藏、调试要不要更友好等问题。并且内联会让代码体积变大,所以在生产环境不用内联

隐藏代码的配置有:

nosources-source-map:全部隐藏。

hidden-source-map:只隐藏源代码,会提示构建后代码错误信息。

生产环境推荐选择:

  • source-map:调试最友好(推荐生成环境使用)。
  • cheap-module-souce-map:速度快。

3. oneOf

正常来讲,一个文件只能被一个loader处理,如果我们不进行配置,那么每个文件就会经过所有的loader判断,这会大大降低我们打包时的效率。

所以我们需要修改一下webpack.config.js文件中的配置。

module: {
  rules: [
    {
      oneOf: [
        // 放在oneOf中的loader,一个文件只会匹配一次,如果被匹配到了就会跳出
        // 相当于js代码中的switch
      ],
    },
  ],
},

4. 缓存

设置文件资源缓存后,浏览器刷新时就不会再重新向服务器请求资源,而是直接从缓存中进行读取,这大大加快了网页的加载速度,但是便利的同时又带来了一个问题,即项目重新部署后,客户端依然不会从服务器进行请求,还是从缓存中进行的读取。

这个时候我们就需要修改打包后的文件名,让浏览器知道该文件已经被修改,从而重新向服务器进行请求。

hash:每次wepack构建时会生成一个唯一的hash值。

问题:因为js和css同时使用一个hash值。如果重新打包,会导致所有缓存失效。(可能我只改动一个文件)。

chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样。

问题:js和css的hash值还是一样的,因为css是在js中被引入的,所以同属于一个chunk。

contenthash:根据文件的内容生成hash值。不同文件hash值一定不一样。

上面几种方法中,最终contenthash满足了我们的需求,所以我们就需要使用contenthash。

// 输出文件
output: {
  // 输出路径
  path: resolve(__dirname, "build"),
  filename: "js/[name]-[contenthash:10].js",
},

5. 代码分割

如果我们有两个js文件入口,并且在两个js文件中引入了同一个node_modules中的模块,那么node_modules中的这个模块会被同时打包到两个js文件中。就相当于一个模块被打包了两次。

所以我们这个时候就需要将node_modules中的模块单独提取到一个文件,如果我们需要用到该模块,直接引入该文件就行,而代码分割就满足我们的需求。

  1. 可以将node_modules中代码单独打包一个chunk最终输出。
  2. 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk。
optimization: {
  splitChunks: {
    chunks: "all",
  },
},

6. 懒加载和预加载

6.1 懒加载

通过import("url")语句,在文件中进行懒加载模块,这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

// 通过js代码,让某个文件被单独打包成一个chunk
// import动态导入语法:能将某个文件单独打包
import(/* webpackChunkName :"test" */"./test")

可以在注释中通过webpackChunkName属性设置打包后的文件名字。

6.2 预加载

预先加载需要调用的模块,跟直接使用ES6 modules import导入语句不同,import导入语句是并行加载,无法实现一个模块在主要的模块加载完毕后再去加载,而预加载则不同,它是在主模块加载完毕后,抽取空余时间进行加载,也就是每次都会优先加载主要的模块,这就不会影响主要模块的加载速度。

通过在注释中设置webpackPrefetch:true就可以实现预加载。

// 通过js代码,让某个文件被单独打包成一个chunk
// import动态导入语法:能将某个文件单独打包
import(/* webpackChunkName :"test",webpackPrefetch:true */"./test")

但是预加载目前存在兼容性问题,这种存在兼容性问题的功能我们一般都不去使用。

7. 多进程打包

在项目文件非常多的时候,打包一次要花费很长的时间,我们就可以使用thread-loader开启多进程打包,一般来讲,我们仅仅对于js文件进行多线程打包,因为一个项目中的js文件一般是非常多的,在一个单页面应用中,几乎全部的代码都是js文件。

使用的方式也非常简单先引入thread-loader:

npm install --save-dev thread-loader

然后在项目中进行配置:

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
    // 多线程打包loader
    "thread-loader",
    {
      loader: "babel-loader",
      options: {
        // 预设:指示babel做怎么样的兼容性处理。
        presets: [
          [
            "@babel/preset-env",
            {
              corejs: {
                version: 3,
              },
              // 按需加载
              useBuiltIns: "usage",
              // 需要适配的最低版本
              targets: {
                chrome: "60",
                firefox: "60",
                ie: "9",
                safari: "10",
                edge: "17",
              },
            },
          ],
        ],
      },
    },
  ],
},

进程启动大概为600ms,同时跨进程的数据交换也会被限制。

只有工作消耗时间比较长,才需要多进程打包,如果本身不大,使用多线程打包反而会更慢。

8. resolve

我们对于项目文件中的模块引入有时候需要进行优化,使得模块的引入更加的方便,所以我们需要配置webpack.config.js中的resolve属性。

resolve: {
  // 配置解析模块路径别名 缺点没有提示
  alias: {
    $css: resolve(__dirname, "src/css"),
  },
  // 配置省略文件路径的后缀名
  extensions: [".js", ".json"],
  // 告诉webpack解析模块是去找哪个目录
  modules: [resolve(__dirname, "../../node_modules"), "node_modules"],
},
  • alias:配置解析模块路径别名,缺点没有提示。
  • extensions:配置省略文件路径的后缀名。
  • modules:告诉webpack解析模块是去找哪个目录。

Vuejs就将@别名设置给了src文件,因为官方提供了插件的原因,所以即便使用别名@进行引入,依然会有提示。

9. 最后

到现在为止,关于webpack的配置我们已经有一定的了解,其实上面这些我们都仅仅是学的webpack工程化的基础内容,只是知道webpack怎么去配置。

而接下来,需要更进阶的学习webpack,比如如何手写一个loader、手写一个plugin,这才是我们需要学习的地方,因为在一般的前端框架中,webpack的基础配置是已经帮我们配置好的,并且它们经过多版本的迭代,webpack的配置也显得十分可靠,根本不需要自己手写关于webpack相关的配置。

所以接下来,我将更加深入的了解loader、plugin,并且学习如何自己写一个loader、plugin。