Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Webpack】Optimize 优化 #44

Open
Tracked by #6
swiftwind0405 opened this issue Apr 14, 2020 · 0 comments
Open
Tracked by #6

【Webpack】Optimize 优化 #44

swiftwind0405 opened this issue Apr 14, 2020 · 0 comments
Labels

Comments

@swiftwind0405
Copy link
Owner

swiftwind0405 commented Apr 14, 2020

保持最新的版本

使用最新版本,带来的优化是最显著的,开发者们会经常进行性能优化。所以第一步先升级 Webpack 以及 所有构建相关 devDependencies 的版本。

保持最新的 Node.js 也能够保证性能。除此之外,保证包管理工具 (例如 npm 或者 yarn ) 为最新也能保证性能。较新的版本能够建立更高效的模块树以及提高解析速度。

量化

必须要有一个量化指标才可以看出前后对比。

speed-measure-webpack-plugin

speed-measure-webpack-plugin 插件可以测量各个插件和 loader 所花费的时间,使用之后,构建时,会得到类似下面这样的信息:

image

对比前后的信息,来确定优化的效果。
speed-measure-webpack-plugin 的使用很简单,可以直接用来包裹 Webpack 的配置:
webpack.config.js

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const config = {
    //...webpack配置
}

module.exports = smp.wrap(config);

Webpack 自带时间统计

上面的 speed-measure-webpack-plugin 主要是测量插件以及 loader 花费的时间(主要是 loader),其自身也是耗时间的,因此在对其分析并相应的做出优化之后,应该去除 speed-measure-webpack-plugin后根据 Webpack 每次打包给出时间作为最后的量化指标。

缩小文件搜索范围(更快命中)

优化 loader 配置

通过排除 node_modules 下的文件 从而缩小了 loader 加载搜索范围 高概率命中文件。为了尽可能少的让文件被 loader 处理,可以通过 include 去命中只有哪些文件需要被处理

优化 resolve.modules 配置

resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块。resolve.modules 的默认值是 ['node modules'],含义是先去当前目录的 ./node modules 目录下去找我们想找的模块,如果没找到,就去上一级目录 ../node modules 中找,再没有就去 ../.. /node modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。当安装的第三方模块都放在项目根目录的 ./node modules 目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。

优化后配置:

resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
modules: [path.resolve(__dirname,'node_modules')]
},

注意:如果配置了上述的 resolve.moudles ,可能会出现问题,例如,依赖中还存在 node_modules 目录,那么就会出现,对应的文件明明在,但是却提示找不到。因此呢,不推荐配置这个。如果其他人不熟悉这个配置,遇到这个问题时,会摸不着头脑。

优化 resolve.alias 配置

创建 import 或 require 的路径别名,来确保模块引入变得更简单。配置项通过别名来把原导入路径映射成一个新的导入路径 此优化方法会影响使用 Tree-Shaking 去除无效代码

优化 resolve.extensions 配置

当引入模块时不带文件后缀 webpack会根据此配置自动解析确定的文件后缀。

  • 后缀列表尽可能小
  • 频率最高的往前放
  • 导出语句尽可能带上后缀
resolve: {
    extensions: ['.js']
}

优化 module.noParse 配置

用了 noParse 的模块将不会被 loaders 解析,所以当我们使用的库如果太大,并且其中不包含 import require define 的调用,我们就可以使用这项配置来提升性能, 让 Webpack 忽略对部分没采用模块化的文件的递归解析处理。

// 忽略对jquery lodash的进行递归解析
module: {
    // noParse: /jquery|lodash/

    // 从 webpack 3.0.0 开始
    noParse: function(content) {
        return /jquery|lodash/.test(content)
    }
}

优化 resolve.mainFields 配置

在安装的第三方模块中都会有一个 package.json 文件,用于描述这个模块的属性,其中可以存在多个字段描述入口文件,原因是某些模块可以同时用于多个环境中,针对不同的运行环境需要使用不同的代码。

resolve.mainFields 的默认值和当前的 target 配置有关系,对应的关系如下。

  • targetweb 或者 webworker 时,值是 ['browser','module','main']
  • target 为其他情况时,值是 ['module','main']

target 等于 web 为例, Webpack 会先采用第三方模块中的 browser 字段去寻找模块的入口文件,如果不存在,就采用 module 字段,以此类推。

为了减少搜索步骤,在明确第三方模块的入口文件描述字段时,可以将它设置得尽量少。由于大多数第三方模块都采用 main 字段去描述入口文件的位置,所以可以这样配置:

module.exports = {
        resolve: {
        //只采用 main 字段作为入口文件的描述字段,以减少搜索步骤
        mainFields: ['main']
        }
    }

优化解析时间(开启多进程打包)

thread-loader

thread-loader 放置在其它 loader 之前,那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。

在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:

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

thread-loader 使用起来也非常简单,只要把 thread-loader 放置在其他 loader 之前, 那 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        // 创建一个 js worker 池
        use: [ 
          'thread-loader',
          'babel-loader'
        ] 
      },
      {
        test: /\.s?css$/,
        exclude: /node_modules/,
        // 创建一个 css worker 池
        use: [
          'style-loader',
          'thread-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      }
      // ...
    ]
    // ...
  }
  // ...
}

官方上说每个 worker 大概都要花费 600ms ,所以官方为了防止启动 worker 时的高延迟,提供了对 worker 池的优化:预热。

// ...
const threadLoader = require('thread-loader');

const jsWorkerPool = {
  // options
  
  // 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
  // 当 require('os').cpus() 是 undefined 时,则为 1
  workers: 2,
  
  // 闲置时定时删除 worker 进程
  // 默认为 500ms
  // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
  poolTimeout: 2000
};

const cssWorkerPool = {
  // 一个 worker 进程中并行执行工作的数量
  // 默认为 20
  workerParallelJobs: 2,
  poolTimeout: 2000
};

threadLoader.warmup(jsWorkerPool, ['babel-loader']);
threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);


module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: jsWorkerPool
          },
          'babel-loader'
        ]
      },
      {
        test: /\.s?css$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
          {
            loader: 'thread-loader',
            options: cssWorkerPool
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      }
      // ...
    ]
    // ...
  }
  // ...
}

注意:

  • thread-loader 放在了 style-loader 之后,这是因为 thread-loader 后的 loader 没法存取文件也没法获取 webpack 的选项设置。
  • 请仅在耗时的 loader 上使用。

HappyPack(已不维护,不推荐 Webpack4 使用)

HappyPack 是让 webpack 对 loader 的执行过程,从单一进程形式扩展为多进程模式,也就是将任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而加速代码构建。与 DLL 动态链接库结合来使用更佳。

合理利用缓存(缩短连续构建时间,增加初始构建时间)

开启缓存之后,在第一构建之后,会产生缓存文件,一般默认缓存目录:node_modules/.cache

cache-loader

将结果缓存到磁盘里,显著提升二次构建速度。

module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: ['cache-loader', ...loaders],
        include: path.resolve('src'),
      },
    ],
  },
};

注意:

  • 保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader。
  • thread-loadercache-loader 要一起使用的話,先放 cache-loader,接着是 thread-loader,最后才是其它的 heavy-loader,这样的順序才可以有最好的效能。
  • 如果只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectorytrue

HardSourceWebpackPlugin

HardSourceWebpackPlugin 为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source

配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。

DllPlugin

在一个动态链接库中可以包含其他模块调用的函数和数据,动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会被重新编译,而是直接使用动态链接库中的代码。

  • 将web应用依赖的基础模块抽离出来,打包到单独的动态链接库中。一个链接库可以包含多个模块。
  • 当需要导入的模块存在于动态链接库,模块不会再次打包,而是去动态链接库中去获取。
  • 页面依赖的所有动态链接库都需要被加载。

如何使用不记录了,因为现在已经不推荐使用 DllPlugin 了,在最新的 webpack 下带来的性能提升有限。

TerserPlugin

Webpack4 默认内置使用 terser-webpack-plugin 插件压缩优化代码,而该插件使用 terser 来缩小 JavaScript。所谓 terser,官方给出的定义是用于 ES6+ 的 JavaScript 解析器、mangler/compressor(压缩器)工具包。

使用多进程并行运行来提高构建速度。并发运行的默认数量为 os.cpus().length - 1

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
};

可以显著加快构建速度,因此强烈推荐开启多进程。

ParallelUglifyPlugin()

Webpack4 已废弃,不推荐 Webpack4 使用。

CSS 代码的压缩

通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。

控制包文件大小

减少编译的整体大小,以提高构建性能。尽量保持 chunks 小巧。

结合stats.json分析打包结果(bundle analyze)

提取页面公共资源

基础包分离:

  • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
  • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件

tree-shaking

打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)。

scope-hosting

构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法

变量提升,可以减少一些变量声明。在生产环境下,默认开启。

另外,大家测试的时候注意一下,speed-measure-webpack-plugin 和 HotModuleReplacementPlugin 不能同时使用,否则会报错。

babel 配置优化

在不配置 @babel/plugin-transform-runtime 时,babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法。默认情况下,它将被注入(inject)到需要它的每个文件中。但是这样的结果就是导致构建出来的JS体积变大。

我们也并不需要在每个 js 中注入辅助函数,因此我们可以使用 @babel/plugin-transform-runtime@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。

因此我们可以在 .babelrc 中增加 @babel/plugin-transform-runtime 的配置。

{
    "presets": [],
    "plugins": [
        [
            "@babel/plugin-transform-runtime"
        ]
    ]
}

图片压缩

开发环境的优化

增量编译

使用 webpack 的监听模式。不要使用其他工具来监听你的文件和调用 webpack 。在监听模式下构建会记录时间戳并将信息传递给编译让缓存失效。

在某些设置中,监听会回退到轮询模式。有许多监听文件会导致 CPU 大量负载。在这些情况下,你可以使用 watchOptions.poll 来增加轮询的间隔。

Devtool

需要注意的是不同的 devtool 的设置,会导致不同的性能差异。

  • "eval" 具有最好的性能,但并不能帮助你转译代码。
  • 如果你能接受稍差一些的 mapping 质量,可以使用 cheap-source-map 选项来提高性能
  • 使用 eval-source-map 配置进行增量编译。

=> 在大多数情况下,cheap-module-eval-source-map 是最好的选择。

避免在生产环境下才会用到的工具

某些实用工具, plugins 和 loaders 都只能在构建生产环境时才有用。例如,在开发时使用 UglifyJsPlugin 来压缩和修改代码是没有意义的。以下这些工具在开发中通常被排除在外:

  • UglifyJsPlugin
  • ExtractTextPlugin
  • [hash] / [chunkhash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

在内存中编译

以下几个实用工具通过在内存中进行代码的编译和资源的提供,但并不写入磁盘来提高性能:

  • webpack-dev-server
  • webpack-hot-middleware
  • webpack-dev-middleware

参考文档

@swiftwind0405 swiftwind0405 changed the title 【Day43】Webpack 的优化 【Day43】【Webpack】优化 Apr 17, 2020
@swiftwind0405 swiftwind0405 changed the title 【Day43】【Webpack】优化 【Webpack】【进阶】优化 Apr 17, 2020
@swiftwind0405 swiftwind0405 changed the title 【Webpack】【进阶】优化 【Webpack】【进阶】Optimize 优化 Apr 17, 2020
@swiftwind0405 swiftwind0405 changed the title 【Webpack】【进阶】Optimize 优化 【Webpack】Optimize 优化 Apr 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant