# 优化

优化

优化分两个场景:开发环境、生产环境

# 通用环境

# 1. loader

将 loader 应用于最少数量的必要模块。通过使用 include 字段,仅将 loader 应用在实际需要将其转换的模块

const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
+       include: path.resolve(__dirname, 'src'),
        loader: 'babel-loader',
      },
    ],
  },
};

# 2. 引导(bootstrap)

每个额外的 loader/plugin 都有其启动时间。尽量少地使用工具。

# 3. 解析(resolve)

以下步骤可以提高解析速度:

  • 减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数。
  • 如果你不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false
  • 如果你使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false

# 4. dll

使用 DllPlugin 为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,尽管它增加了构建过程的复杂度。

# 开发环境优化

# 1.自动编译代码

每次编译代码时,要手动 npm run build会显得很麻烦。webpack提供了几种可选方式,帮助你在代码发生变化后**自动编译代码 (opens new window)**:

  • webpck的 watch model(增量编译)
  • webpack-dev-server(内存编译 - 推荐)
  • webpack-dev-middleware

# a. webpack watch mode(观察模式)

{
  "scripts": {
      "build": "webpack",
+     "watch": "webpack --watch"
  }
}
  • 缺点:为了看到修改后的实际效果,需要刷新浏览器。

# b. webpack-dev-server

webpack-dev-server为你提供了一个简单的 web server,并且具有 live reloading(实时重新加载)功能。设置如下:

npm i webpack-dev-server -D

修改配置文件,告知dev server从什么位置查找文件:

// webpack.config.js
module.exports = {
  mode: 'development',
+ devServer: {
+   contentBase: './dist',
+ }
}

以上配置告知 webpack-dev-server,将dist 目录下的文件 servelocalhost:8080 下。

WARNING

webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。如果你的页面希望在其他不同路径中找到 bundle 文件,则可以通过 dev server 配置中的 publicPath (opens new window) 选项进行修改。

我们添加一个可以直接运行 dev server 的 script:

{
  "scripts": {
      "build": "webpack",
      "watch": "webpack --watch"
+     "start": "webpack serve --open"
  }
}

webpack-dev-server 具有许多可配置的选项。关于其他更多配置,请查看 配置文档 (opens new window)

TIP

现在,server 正在运行,你可能需要尝试 模块热替换(hot module replacement) (opens new window)

模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。我们要做的就是更新 webpack-dev-server (opens new window) 配置, 然后使用 webpack 内置的 HMR 插件。

// webpack.config.js
module.exports = {
  mode: 'development',
  devServer: {
    contentBase: './dist',
+   hot: true,
  }
}

# c.webpack-dev-middleware 中间件

webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。

webpack-dev-middleware (opens new window)中间件配合 express server使用

# 生产环境优化

# 1. 代码分离

代码分离 (opens new window)是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

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

# SplitChunksPlugin

SplitChunksPlugin (opens new window) 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将之前的示例中重复的 lodash 模块去除:

webpack.config.js

  const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js',
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all',
+     },
+   },
  };

使用 optimization.splitChunks (opens new window) 配置选项之后,现在应该可以看出,index.bundle.jsanother.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。

# bundle 分析(bundle analysis)

一旦开始分离代码,一件很有帮助的事情是,分析输出结果来检查模块在何处结束。 官方分析工具 (opens new window) 是一个不错的开始。还有一些其他社区支持的可选项:

# 2. 缓存

可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。

此指南的重点在于通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存 (opens new window),而在文件内容变化后,能够请求到新的文件。

# 输出文件的文件名(output filename)

我们可以通过替换 output.filename 中的 substitutions (opens new window) 设置,来定义输出文件的名称。webpack 提供了一种使用称为 substitution(可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。其中,[contenthash] substitution 将根据资源内容创建出唯一 hash。当资源内容发生变化时,[contenthash] 也会发生变化。

 output: {
-  filename: 'bundle.js',
+  filename: '[name].[contenthash].js',
   path: path.resolve(__dirname, 'dist'),
   clean: true,
 },

# 3. Tree Shaking

术语

tree shaking (opens new window) 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 (opens new window) 特性,例如 import (opens new window)export (opens new window)。这个术语和概念实际上是由 ES2015 模块打包工具 rollup (opens new window) 普及起来的。

webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json"sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。

# 解释 tree shaking 和 sideEffects

sideEffects (opens new window)usedExports (opens new window)(更多被认为是 tree shaking)是两种不同的优化方式。

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

usedExports 依赖于 terser (opens new window) 去检测语句中的副作用。它是一个 JavaScript 任务而且没有像 sideEffects 一样简单直接。而且它不能跳转子树/依赖由于细则中说副作用需要被评估。尽管导出函数能运作如常,但 React 框架的高阶函数(HOC)在这种情况下是会出问题的。

# tree sharking小结

因此,我们学到为了利用 tree shaking 的优势, 你必须...

  • 使用 ES2015 模块语法(即 importexport)。
  • 确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅文档 (opens new window))。
  • 在项目的 package.json 文件中,添加 "sideEffects" 属性。
  • 使用 mode"production" 的配置项以启用更多优化项 (opens new window),包括压缩代码与 tree shaking。

你可以将应用程序想象成一棵树。绿色表示实际用到的 source code(源码) 和 library(库),是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。

如果你对优化输出很感兴趣,请进入到下个指南,来了解 生产环境 (opens new window) 构建的详细细节。

# 4.源码映射(Source Mapping)

我们鼓励你在生产环境中启用 source map,因为它们对 debug(调试源码) 和运行 benchmark tests(基准测试) 很有帮助。虽然有着如此强大的功能,然而还是应该针对生产环境用途,选择一个可以快速构建的推荐配置(更多选项请查看 devtool (opens new window))。对于本指南,我们将在 生产环境 中使用 source-map 选项,而不是我们在 开发环境 中用到的 inline-source-map

webpack.prod.js

  const { merge } = require('webpack-merge');
  const common = require('./webpack.common.js');

  module.exports = merge(common, {
    mode: 'production',
+   devtool: 'source-map',
  });

# 5.压缩 CSS

将生产环境下的 CSS 进行压缩会非常重要,请查看 在生产环境下压缩 (opens new window) 章节。

为了压缩输出文件,请使用类似于 css-minimizer-webpack-plugin (opens new window) 这样的插件

webpack.prod.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
  optimization: {
    minimizer: [
      // For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
      // `...`,
      new CssMinimizerPlugin(),
    ],
  },
};

这将只在生产模式下启用 CSS 压缩优化。如果你需要在开发模式下使用,请设置 optimization.minimize 选项为 true。

# 6. CDN加速

方式

不怎么需要更新第三方库脱离webpack打包,不被打入bundle中,从而减少打包时间,但又不影响运用第三方库的方式,例如import方式等

  • 方式一:使用 html-webpack-externals-plugin
  • 方式二:直接配置 externals

# html-webpack-externals-plugin

通常在 webpack.base.conf.js 中进行配置,让其不打包到 vendor.js 中,配置如下:(以vue框架为例)

const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');

module.exports = {
    // 其它省略...
    plugins: [
        new HtmlWebpackExternalsPlugin({
          externals: [{
            module: 'vue',
            entry: 'https://lib.baomitu.com/vue/2.6.12/vue.min.js',
            global: 'Vue'
          }]
        })
    ],
    // 其它省略...
}

最后看到在 index.html 中动态添加了如下代码:

<script type="text/javascript" src="https://lib.baomitu.com/vue/2.6.12/vue.min.js"></script>

# externals配置

首先在 index.html 中script标签引入JS,如下代码:

<script type="text/javascript" src="https://lib.baomitu.com/vue/2.6.12/vue.min.js"></script>

webpack.base.conf.js 的配置如下:

module.exports = {
    // 其它省略...
    externals: {
        vue: 'Vue'
    },
    // 其它省略...
}

# 总结

development(开发环境)production(生产环境) 这两个环境下的构建目标存在着巨大差异。

开发环境

开发环境中,我们需要:强大的 source map 和一个有着 live reloading(实时重新加载) 或 hot module replacement(热模块替换) 能力的 localhost server。

生产环境

生产环境目标则转移至其他方面,关注点在于压缩 bundle、更轻量的 source map、资源优化等,通过这些优化方式改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置

webpack-merge

虽然,以上我们将 生产环境开发环境 做了细微区分,但是,请注意,我们还是会遵循不重复原则(Don't repeat yourself - DRY),保留一个 "common(通用)" 配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge (opens new window) 的工具。此工具会引用 "common" 配置,因此我们不必再在环境特定(environment-specific)的配置中编写重复代码。

{
    "name": "development",
    "version": "1.0.0",
    "description": "",
    "main": "src/index.js",
    "scripts": {
-    "start": "webpack serve --open",
+    "start": "webpack serve --open --config webpack.dev.js",
-    "build": "webpack"
+    "build": "webpack --config webpack.prod.js"
},

# 参考文献

Last Updated: 7/15/2021, 11:46:08 AM