关闭

如何十倍提高你的webpack构建效率

标签: 架构体验webpack
7882人阅读 评论(1) 收藏 举报
分类:

前言

webpack 是个好东西,和 NPM 搭配起来使用管理模块实在非常方便。而 Babel 更是神一般的存在,让我们在这个浏览器尚未全面普及 ES6 语法的时代可以先一步体验到新的语法带来的便利和效率上的提升。在 React 项目架构中这两个东西基本成为了标配,但 commonjs 的模块必须在使用前经过 webpack 的构建(后文称为 build)才能在浏览器端使用,而每次修改也都需要重新构建(后文称为 rebuild)才能生效,如何提高 webpack 的构建效率成为了提高开发效率的关键之一。

ES6 + Webpack + React + Babel

Webpack 的构建流程

在开始正式的优化之前,让我们先回顾一下 Webpack 的构建流程,有哪些关键步骤,只有了解了这些,我们才能分析出哪些地方有优化的可能性。

webpack is a module bundler

首先,我们来看看官方对于 Webpack 的理念阐释,webapck 把所有的静态资源都看做是一个 module,通过 webpack,将这些 module 组成到一个 bundle 中去,从而实现在页面上引入一个 bundle.js,来实现所有静态资源的加载。所以详细一点看,webpack 应该是这样的:

Every static asset should be able to be a module --webpack

通过 loaderwebpack 可以把各种非原生 js 的静态资源转换成 JavaScript,所以理论上任何一种静态资源都可以成为一个 module
当然 webpack 还有很多其他好玩的特性,但不是本文的重点因此不铺开进行说明了。了解了上述的过程,我们就可以根据这些过程的前后处理进行对应的优化,接下来我们会针对 build 和 rebuild 的过程给与相应的意见。

RESOLVE

我们先从解析模块路径和分析依赖讲起,有人可能觉得这无所谓,但当项目应用依赖的模块越来越多,越来越重时,项目越来越大,文件和文件夹越来越多时,这个过程就变得越来越关乎性能。

减小 Webpack 覆盖的范围

build +, rebuild +

webpack 默认会去寻找所有 resolve.root 下的模块,但是有些目录我们是可以明确告知 webpack 不要管这里,从而减轻 webpack 的工作量。这时会用到 module.noParse 参数。

Resolove.root VS Resolove.moduledirectories

build +, rebuild +

rootmoduledirectories 如果只从用法上来看,似乎是可以互相替代的。但因为 moduledirectories 从设计上是取相对路径,所以比起 root ,所以会多 parse 很多路径。

resolve: {  
    root: path.resolve('src'),
    modulesDirectories: ['node_modules'],
    extensions: ['', '.js', '.jsx']
},
resolve: {  
    modulesDirectories: ['node_modules', './src'],
    extensions: ['', '.js', '.jsx']
},

上面的配置,只会解析 :

/some/src/other/folder/node_modules/a

而下面的配置会解析 :

/some/folder/structure/node_modules/a
/some/folder/structure/src/a
/some/folder/node_modules/a
/some/folder/src/a
/some/node_modules/a
/some/src/a
/node_modules/a
/src/a 

大部分的情况下使用 root 即可,只有在有很复杂的路径下,才考虑使用 moduledirectories,这可以明显提高 webpack 的构建性能。这个 issue 也很详细地讨论了这个问题。

LOADERS

webpack 官方和社区为我们提供了各种各样 loader 来处理各种类型的文件,这些 loader 的配置也直接影响了构建的性能。

Babel-loader: 能者少劳

build ++, rebuild ++

babel-loader 为例,我们在开发 React 项目时很可能会使用到了 ES6 或者 jsx 的语法,因此使用到 babel-loader 的情况很多,最简单的情况下我们可以这样配置,让所有的 js/jsx 通过 babel-loader

module: {  
    loaders: [
      {
          test: /\.js(x)*$/,
          loader: 'babel-loader',
          query: {
              presets: ['react', 'es2015-ie', 'stage-1']
          }
      }
    ]
}

上面这样的做法当然是 ok 的,但是对于很多的 npm 包来说,他们完全没有经过 babel 的必要(成熟的 npm 包会在发布前将自己 es5,甚至 es3 化),让这些包通过 babel 会带来巨大的性能负担,毕竟 babel6 要经过几十个插件的处理,虽然 babel-loader 强大,但能者多劳的这种保守的想法却使得 babel-loader 成为了整个构建的性能瓶颈。所以我们可以使用 exclude,大胆地屏蔽掉 npm 里的包,从而使整包的构建效率飞速提高。

module: {  
    loaders: [
      {
          test: /\.js(x)*$/,
          loader: 'babel-loader',
          exclude: function(path) {
              // 路径中含有 node_modules 的就不去解析。
              var isNpmModule = !!path.match(node_modules/);
              return isNpmModule;
          },
          query: {
              presets: ['react', 'es2015-ie', 'stage-1']
          }
      }
    ]
}

甚至,在我们十分确信的情况下,使用 include 来限定 babel 的使用范围,进一步提高效率。

var path = require('path');  
module.exports = {  
    module: {
        loaders: [
          {
              test: /\.js(x)*$/,
              loader: 'babel-loader',
              include: [
                // 只去解析运行目录下的 src 和 demo 文件夹
                path.join(process.cwd(), './src'),
                path.join(process.cwd(), './demo')
              ],
              query: {
                  presets: ['react', 'es2015-ie', 'stage-1']
              }
          }
        ]
    }
}

PLUGINS

webpack 官方和社区为我们提供了很多方便的插件,有些插件为我们开发和生产带来了很多的便利,但是不合适地使用插件也会拖慢 webpack 的构建效率,而有些插件虽然不会为我们的开发上直接提供便利,但使用他们却可以帮助我们提高 webpack 的构建效率,这也是本文会提到的。

SourceMaps

build +

SourceMaps 是一个非常实用的功能,可以让我们在 chrome debug 时可以不用直接看已经 bundle 过的 js,而是直接在源代码上进行查看和调试,但完美的 SourceMaps 是很慢的,webpack 官方提供了七种 sourceMap 模式共大家选择,性能对比如下:

devtool build speed rebuild speed production supported quality
eval +++ +++ no generated code
cheap-eval-source-map + ++ no transformed code (lines only)
cheap-source-map + o yes transformed code (lines only)
cheap-module-eval-source-map o ++ no original source (lines only)
cheap-module-source-map o - yes original source (lines only)
eval-source-map + no original source
source-map yes original source

具体各自的区别请参考 https://github.com/webpack/docs/wiki/configuration#devtool ,我们这里推荐使用 cheap-source-map,也就是去掉了column mapping 和 loader-sourceMap(例如 jsx to js) 的 sourceMap,虽然带上 eval 参数的可以快更多,但是这种 sourceMap 只能看,不能调试,得不偿失。

OPTIMIZATION

build ++,rebuild ++

webpack 提供了一些可以优化浏览器端性能的优化插件,如UglifyJsPlugin,OccurrenceOrderPlugin 和 DedupePlugin,都很实用,也都在消耗构建性能(UglifyJsPlugin 非常耗性能),如果你是在开发环境下,这些插件最好都不要使用,毕竟脚本大一些,跑的慢一些这些比起每次构建要耗费更多时间来说,显然还是后者更会消磨开发者的耐心,因此,只在正产环境中使用 OPTIMIZATION。

CommonsChunk

rebuild +

当你的 webpack 构建任务中有多个入口文件,而这些文件都 require 了相同的模块,如果你不做任何事情,webpack 会为每个入口文件引入一份相同的模块,显然这样做,会使得相同模块变化时,所有引入的 entry 都需要一次 rebuild,造成了性能的浪费,CommonsChunkPlugin 可以将相同的模块提取出来单独打包,进而减小 rebuild 时的性能消耗。这里有一篇很通俗易懂的使用方法:http://webpack.toobug.net/zh-cn/chapter3/common-chunks-plugin.html ,感兴趣的朋友不妨一试。

DLL & DllReference

build +++, rebuild +++

除了正在开发的源代码之外,通常还会引入很多第三方 NPM 包,这些包我们不会进行修改,但是仍然需要在每次 build 的过程中消耗构建性能,那有没有什么办法可以减少这些消耗呢?DLLPlugin 就是一个解决方案,他通过前置这些依赖包的构建,来提高真正的 buildrebuild 的构建效率。 鉴于现有的资料对于这两个插件的解释都不是很清楚,笔者这里翻译了一篇日本同学的文章,通过一个简单的例子来说明一下这两个插件的用法。我们举例,把 reactreact-dom 打包成为 dll bundle。 首先,我们来写一个 DLLPluginconfig 文件。

webpack.dll.config.js

const path = require('path');  
const webpack = require('webpack');

module.exports = {  
  entry: {
    vendor: ['react', 'react-dom']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].dll.js',
    /**
     * output.library
     * 将会定义为 window.${output.library}
     * 在这次的例子中,将会定义为`window.vendor_library`
     */
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      /**
       * path
       * 定义 manifest 文件生成的位置
       * [name]的部分由entry的名字替换
       */
      path: path.join(__dirname, 'dist', '[name]-manifest.json'),
      /**
       * name
       * dll bundle 输出到那个全局变量上
       * 和 output.library 一样即可。 
       */
      name: '[name]_library'
    })
  ]
};

执行 webpack 后,就会在 dist 目录下生成 dll bundle 和对应的 manifest 文件

$ ./node_modules/.bin/webpack --config webpack.dll.config.js
Hash: 36187493b1d9a06b228d  
Version: webpack 1.13.1  
Time: 860ms  
        Asset    Size  Chunks             Chunk Names
vendor.dll.js  699 kB       0  [emitted]  vendor  
   [0] dll vendor 12 bytes {0} [built]
    + 167 hidden modules

$ ls dist
./                    vendor-manifest.json
../                   vendor.dll.js

manifest 文件的格式大致如下,由包含的 module 和对应的 id 的键值对构成。

cat dist/vendor-manifest.json  
{
  "name": "vendor_library",
  "content": {
    "./node_modules/react/react.js": 1,
    "./node_modules/react/lib/React.js": 2,
    "./node_modules/process/browser.js": 3,
    "./node_modules/object-assign/index.js": 4,
    "./node_modules/react/lib/ReactChildren.js": 5,
    "./node_modules/react/lib/PooledClass.js": 6,
    "./node_modules/fbjs/lib/invariant.js": 7,
...

好,接下来我们通过 DLLReferencePlugin 来使用刚才生成的 DLL Bundle

首先我们写一个只去 require react,并通过 console.log 吐出的 index.js

var React = require('react');  
var ReactDOM = require('react-dom');  
console.log("dll's React:", React);  
console.log("dll's ReactDOM:", ReactDOM);  

再写一个不参考 Dll Bundle 的普通 webpack config 文件。

webpack.conf.js

const path = require('path');  
const webpack = require('webpack');

module.exports = {  
  entry: {
    'dll-user': ['./index.js']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].bundle.js'
  }
};

执行 webpack,会在 dist 下生成 dll-user.bundle.js,约 700K,耗时 801ms。

$ ./node_modules/.bin/webpack
Hash: d8cab39e58c13b9713a6  
Version: webpack 1.13.1  
Time: 801ms  
             Asset    Size  Chunks             Chunk Names
dll-user.bundle.js  700 kB       0  [emitted]  dll-user  
   [0] multi dll-user 28 bytes {0} [built]
   [1] ./index.js 145 bytes {0} [built]
    + 167 hidden modules

接下来,我们加入 DLLReferencePlugin

webpack.conf.js

const path = require('path');  
const webpack = require('webpack');

module.exports = {  
  entry: {
    'dll-user': ['./index.js']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].bundle.js'
  },
  // ----在这里追加----
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      /**
       * 在这里引入 manifest 文件
       */
      manifest: require('./dist/vendor-manifest.json')
    })
  ]
  // ----在这里追加----
};
./node_modules/.bin/webpack
Hash: 3bc7bf760779b4ca8523  
Version: webpack 1.13.1  
Time: 70ms  
             Asset     Size  Chunks             Chunk Names
dll-user.bundle.js  2.01 kB       0  [emitted]  dll-user  
   [0] multi dll-user 28 bytes {0} [built]
   [1] ./index.js 145 bytes {0} [built]
    + 3 hidden modules

结果是非常惊人的,只有2.01K,耗时 70 ms,无疑大大提高了 buildrebuild 的效率。实际放到页面上看下是否可行。

<body>  
  <script src="dist/vendor.dll.js"></script>
  <script src="dist/dll-user.bundle.js"></script>
</body>  

因为 Dll bundle 在依赖安装完毕后就可以进行了,我们可以在第一次执行 dev server 前执行一次 dll bundlewebapck 任务。

和 external 的比较

有人会说,这个和 用 webpackexternals 配置把 requiremodule 指向全局变量有点像啊。

const path = require('path');  
const webpack = require('webpack');

module.exports = {  
  entry: {
    'ex': ['./index.js']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].bundle.js'
  },
  externals: {
    // require('react')はwindow.Reactを使う
    'react': 'React',
    // require('react-dom')はwindow.ReactDOMを使う
    'react-dom': 'ReactDOM'
  }
};
<body>  
  <script src="dist/react.min.js"></script>
  <script src="dist/react-dom.min.js"></script>
  <script src="dist/ex.bundle.js"></script>
</body>  

这里有两个主要的区别:

  1. 像是 react 这种已经打好了生产包的使用 externals 很方便,但是也有很多 npm 包是没有提供的,这种情况下 DLLBundle 仍可以使用。
  2. 如果只是引入 npm 包一部分的功能,比如 require('react/lib/React') 或者 require('lodash/fp/extend') ,这种情况下 DLLBundle 仍可以使用。
  3. 当然如果只是引用了 react 这类的话,externals 因为配置简单所以也推荐使用。

HappyPack

build +, rebuild +

webpack 的长时间构建搞的大家都很 unhappy。于是 @amireh 想到了一个点子,既然 loader 默认都是一个进程在跑,那是否可以让 loader 多进程去处理文件呢?

这里写图片描述

happyPack 的文档写的很易懂,这里就不再赘述,happyPack 不仅利用了多进程,同时还利用缓存来使得 rebuild 更快。下面是插件作者给出的性能数据:

For the main repository I tested on, which had around 3067 modules, the build time went down from 39 seconds to a whopping ~10 seconds when there was yet no cache. Successive builds now take between 6 and 7 seconds.

Here’s a rundown of the various states the build was performed in:

Elapsed (ms) Happy? Cache enabled? Cache present? Using DLLs?
39851 NO N/A N/A NO
37393 NO N/A N/A YES
14605 YES NO N/A NO
13925 YES YES NO NO
11877 YES YES YES NO
9228 YES NO N/A YES
9597 YES YES NO YES
6975 YES YES YES YES

The builds above were run on Linux over a machine with 12 cores.

其他

上面我们针对 webpackresolveloaderplugin 的过程给出了相应的优化意见,除了这些哪些优化点呢?其实有些优化贯穿在这个流程中,比如缓存和文件 IO。

Cache

无论在何种性能优化中,缓存总是必不可少的一部分,毕竟每次变动都只影响很小的一部分,如果能够缓存住那些没有变动的部分,直接拿来使用,自然会事半功倍,在 webpack 的整个构建过程中,有多个地方提供了缓存的机会,如果我们打开了这些缓存,会大大加速我们的构建,尤其是 rebuild 的效率。

webpack.cache

rebuild +

webpack 自身就有 cache 的配置,并且在 watch 模式下自动开启,虽然效果不是最明显的,但却对所有的 module 都有效。

babel-loader.cacheDirectory

rebuild ++

babel-loader 可以利用系统的临时文件夹缓存经过 babel 处理好的模块,对于 rebuild js 有着非常大的性能提升。

HappyPack.cache

build +, rebuild +

上面提到的 happyPack 插件也同样提供了 cache 功能,默认是以 .happypack/cache--[id].json 的路径进行缓存。因为是缓存在当前目录下,所以他也可以辅助下次 build 时的效率。

FileSystem

默认的情况下,构建好的目录一定要输出到某个目录下面才能使用,但 webpack 提供了一种很棒的读写机制,使得我们可以直接在内存中进行读写,从而极大地提高 IO 的效率,开启的方法也很简单。

var MemoryFS = require("memory-fs");  
var webpack = require("webpack");

var fs = new MemoryFS();  
var compiler = webpack({ ... });  
compiler.outputFileSystem = fs;  
compiler.run(function(err, stats) {  
  // ...
  var fileContent = fs.readFileSync("...");
});

当然,我们还可以通过 webpackDevMiddleware 更加无缝地就接入到 dev server 中,例如我们以 express 作为静态 server 的例子。

var compiler = webpack(webpackCfg);

var webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, {  
   // webpackDevMiddleware 默认使用了 memory-fs
   publicPath: '/dist',
   aggregateTimeout: 300, // wait so long for more changes
   poll: true, // use polling instead of native watchers
   stats: {
       chunks: false
   }
});

var app = express();  
app.use(webpackDevMiddlewareInstance);  
app.listen(xxxx, function(err) {  
   console.log(colors.info("dev server start: listening at " + xxxx));
   if (err) {
     console.error(err);
   }
}

总结

上面我们从 webpack 构建的各个部分,给出了相应的优化策略,如果你的项目中能够将其完全贯彻起来,10 倍提速不是梦想。这些优化也同样应用到了我们团队的 react 项目中,https://github.com/uxcore/uxcore ,欢迎一起来讨论 webpack 的效率优化方案。

参考文章

本文作者 eternalsky,始发于团队微信公众号 猿猿相抱 和个人博客 空の屋敷,转载请保留作者信息。

3
0
查看评论

Webpack noParse参数

module.noParse RegExp | [RegExp]RegExp | [RegExp] | function(从 webpack 3.0.0 开始)防止 webpack 解析那些任何与给定正则表达式相匹配的文件。忽略的文件中不应该含有 import, require, define 的...
  • MrHelicopter
  • MrHelicopter
  • 2017-11-10 14:41
  • 343

Webpack入门

Webpack 是什么webpack是一款模块加载器兼打包工具,它能把各种资源,例如JS(含JSX)、coffee、样式(含less/sass)、图片等都作为模块来使用和处理。 简单说就是模块加载器,通过使用Webpack,能够像Node.js一样处理依赖关系,然后解析出模块之间的依赖,将代码打包...
  • liujie19901217
  • liujie19901217
  • 2016-04-05 11:35
  • 7983

webpack3学习笔记

webpack 是前端开发者一个跨不过去的编译工具。不过由于他的快速迭代,让很多同学在学一个版本的时候,下一个新版本就发布了,让人感觉非常蛋疼和无奈:“我是谁,我在干嘛,我要做什么?”不过,如果你已经掌握 webpack 比较老的版本,对于新版本的学习而言,应该只需要 0.5 天的工作量就可以完成。...
  • osdfhv
  • osdfhv
  • 2018-01-09 20:45
  • 142

webpack 使用总结

前言 本文是对近半年使用 webpack 的一个总结。webpack 作为 配置型的工具,虽然配置项很多、功能强大,但是用起来并不复杂。文内主要内容来自网络摘抄以及翻译官网文档,相对比较全面。 目前 webpack 已经更新到 2.x 版本,不过 1.x 与 2.x 两个大版本之间的区别并不...
  • u010537398
  • u010537398
  • 2017-02-22 14:47
  • 1406

vue 快速入门与最佳实践(vue-cli, webpack, vue 最佳实践)

如何使用vue-cli 快速搭建vue 项目 webpack 原理解读 订阅发布模式,响应式数据流 vue 生命周期 组件定义的方式 父子组件通信机制 非父子组件通信 slot 分发内容 vue-router 路由与常用钩子 vuex 状态管理
  • du_peiduo
  • du_peiduo
  • 2017-03-22 15:42
  • 2189

webpack性能优化——DLL

Webpack性能优化的方式有很多种,本文之所以将 dll 单独讲解,是因为 dll 是一种最简单粗暴并且极其有效的优化方式。 在通常的打包过程中,你所引用的诸如:jquery、bootstrap、react、react-router、redux、antd、vue、vue-router、vuex ...
  • ailongyang
  • ailongyang
  • 2017-03-02 13:55
  • 2251

webpack性能优化

使用webpack也有一段时间了,从webpack1到webpack3,这中间有太多的曲折与纠结。webpack无疑是强大的,强大到没有朋友,webpack的设计初衷也是伟大的,它可以编译打包我们的代码,借助loader,使得我们可以使用各种es6/es7的特性,可以使用sass/less,可以编写...
  • Amosssss
  • Amosssss
  • 2017-08-01 01:39
  • 388

彻底解决Webpack打包慢的问题

转载自:https://segmentfault.com/a/1190000006087638?utm_source=weekly&utm_medium=email&utm_campaign=email_weekly 这几天写腾讯实习生 Mini 项目的时候用上了 React ...
  • fengyinchao
  • fengyinchao
  • 2016-08-03 09:20
  • 15939

使用 webpack 优化资源

https://www.tuicool.com/articles/MvauquR 前言 在前端应用的优化中,对加载资源的大小控制极其的重要,大多数时候我们能做的是在打包编译的过程对资源进行大小控制、拆分与复用。 本片文章中主要是基于 webpack 打包,以 React、vue...
  • sinat_17775997
  • sinat_17775997
  • 2017-10-27 09:10
  • 284

webpack 代码压缩优化篇

Webpack 默认提供的 UglifyJS 插件,由于采用单线程压缩,速度颇慢 ;推荐采用 webpack-parallel-uglify-plugin 插件,她可以并行运行 UglifyJS 插件,更加充分而合理的使用 CPU 资源,这可以大大减少的构建时间;当然,该插件应用于生产环境而非开发环...
  • Deng_gene
  • Deng_gene
  • 2017-10-24 17:45
  • 882
    个人资料
    • 访问:294575次
    • 积分:3693
    • 等级:
    • 排名:第10422名
    • 原创:95篇
    • 转载:13篇
    • 译文:2篇
    • 评论:56条
    文章分类
    最新评论