Skip to content

Webpack构建优化——缩小文件搜索范围

罗学 edited this page Dec 26, 2019 · 1 revision

原文 https://www.jianshu.com/p/bd96b2f25a8e

缩小文件搜索范围

Webpack启动后会从配置的Entry出发,解析出文件中的导入语句,再递归的解析。 在遇到导入语句时Webpack会做两件事情:

  1. 根据导入语句去寻找对应的要导入的文件。例如require('react')导入语句对应的文件是./node_modules/react/react.jsrequire('./util')对应的文件是./util.js
  2. 根据找到的要导入文件的后缀,使用配置中的Loader去处理文件。例如使用ES6开发的JavaScript文件需要使用babel-loader去处理。

以上两件事情虽然对于处理一个文件非常快,但是当项目大了以后文件量会变的非常多,这时候构建速度慢的问题就会暴露出来。虽然以上两件事情无法避免,但需要尽量减少以上两件事情的发生,以提高速度。

优化loader配置

由于Loader对文件的转换操作很耗时,需要让尽可能少的文件被Loader处理。
在使用Loader时可以通过testincludeexclude三个配置项来命中Loader要应用规则的文件。为了尽可能少的让文件被Loader处理,可以通过include去命中只有哪些文件需要被处理。
以采用ES6的项目为例,在配置babel-loader时,可以这样:

module.exports = {
  module: {
    rules: [
      {
        // 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
        test: /\.js$/,
        // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
        use: ['babel-loader?cacheDirectory'],
        // 只对项目根目录下的 src 目录中的文件采用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
    ]
  },
};

优化resolve.modules配置

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

module.exports = {
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    // 其中 __dirname 表示当前工作目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')]
  },
};

优化resolve.mainFields配置

resolve.mainFields用于配置第三方模块使用哪个入口文件。
安装的第三方模块中都会有一个package.json文件用于描述这个模块的属性,其中有些字段用于描述入口文件在哪里,resolve.mainFields用于配置采用哪个字段作为入口文件的描述。
可以存在多个字段描述入口文件的原因是因为有些模块可以同时用在多个环境中,针对不同的运行环境需要使用不同的代码。 以isomorphic-fetch为例,它是fetch API的一个实现,但可同时用于浏览器和Node.js环境。 它的package.json中就有2个入口文件描述字段:

{
  "browser": "fetch-npm-browserify.js",
  "main": "fetch-npm-node.js"
}

isomorphic-fetch在不同的运行环境下使用不同的代码是因为fetch API的实现机制不一样,在浏览器中通过原生的fetch或者XMLHttpRequest实现,在Node.js中通过http模块实现。

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

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

target等于web为例,Webpack会先采用第三方模块中的browser字段去寻找模块的入口文件,如果不存在就采用module字段,以此类推。
为了减少搜索步骤,在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少。 由于大多数第三方模块都采用main字段去描述入口文件的位置,可以这样配置Webpack:

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

使用本方法优化时,你需要考虑到所有运行时依赖的第三方模块的入口文件描述字段,就算有一个模块搞错了都可能会造成构建出的代码无法正常运行。

优化 resolve.alias 配置

resolve.alias配置项通过别名来把原导入路径映射成一个新的导入路径。
在实战项目中经常会依赖一些庞大的第三方模块,以React库为例,安装到node_modules目录下的React库的目录结构如下:

├── dist
│   ├── react.js
│   └── react.min.js
├── lib
│   ... 还有几十个文件被忽略
│   ├── LinkedStateMixin.js
│   ├── createClass.js
│   └── React.js
├── package.json
└── react.js

可以看到发布出去的React库中包含两套代码:

  • 一套是采用CommonJS规范的模块化代码,这些文件都放在lib目录下,以package.json中指定的入口文件react.js为模块的入口。
  • 一套是把React所有相关的代码打包好的完整代码放到一个单独的文件中,这些代码没有采用模块化可以直接执行。其中dist/react.js是用于开发环境,里面包含检查和警告的代码。dist/react.min.js是用于线上环境,被最小化了。
    默认情况下Webpack会从入口文件./node_modules/react/react.js开始递归的解析和处理依赖的几十个文件,这会时一个耗时的操作。 通过配置resolve.alias可以让Webpack在处理React库时,直接使用单独完整的react.min.js文件,从而跳过耗时的递归解析操作。
module.exports = {
  resolve: {
    // 使用 alias 把导入 react 的语句换成直接使用单独完整的 react.min.js 文件,
    // 减少耗时的递归解析操作
    alias: {
      'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
    }
  },
};

除了React库外,大多数库发布到Npm仓库中时都会包含打包好的完整文件,对于这些库你也可以对它们配置alias
但是对于有些库使用本优化方法后会影响到使用Tree-Shaking去除无效代码的优化,因为打包好的完整文件中有部分代码你的项目可能永远用不上。 一般对整体性比较强的库采用本方法优化,因为完整文件中的代码是一个整体,每一行都是不可或缺的。 但是对于一些工具类的库,例如lodash,你的项目可能只用到了其中几个工具函数,你就不能使用本方法去优化,因为这会导致你的输出代码中包含很多永远不会执行的代码。

优化resolve.extensions配置

在导入语句没带文件后缀时,Webpack会自动带上后缀后去尝试询问文件是否存在。resolve.extensions用于配置在尝试过程中用到的后缀列表,默认是:

extensions: ['.js', '.json']

也就是说当遇到require('./data')这样的导入语句时,Webpack会先去寻找./data.js文件,如果该文件不存在就去寻找./data.json文件,如果还是找不到就报错。
如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以resolve.extensions的配置也会影响到构建的性能。 在配置resolve.extensions时你需要遵守以下几点,以做到尽可能的优化构建性能:

  • 后缀尝试列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
  • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。例如在你确定的情况下把require('./data')写成require('./data.json')
module.exports = {
  resolve: {
    // 尽可能的减少后缀尝试的可能性
    extensions: ['js'],
  },
};

优化module.noParse配置

module.noParse配置项可以让Webpack忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库,例如jQuery、ChartJS,它们庞大又没有采用模块化标准,让Webpack去解析这些文件耗时又没有意义。
例如单独完整的react.min.js文件就没有采用模块化,我们可以通过配置module.noParse忽略对react.min.js文件的递归解析处理,相关Webpack配置如下:

const path = require('path');
module.exports = {
  module: {
    // 独完整的react.min.js文件就没有采用模块化,忽略对react.min.js文件的递归解析处理
    noParse: [/react\.min\.js$/],
  },
};

注意被忽略掉的文件里不应该包含importrequiredefine等模块化语句,不然会导致构建出的代码中包含无法在浏览器环境下执行的模块化语句。