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

Boost webpack build performance | Optimising webpack build performance | Webpack 构建性能优化探索 #1

Open
soda-x opened this issue Aug 28, 2016 · 25 comments
Labels

Comments

@soda-x
Copy link
Owner

soda-x commented Aug 28, 2016

前言

webpack 是什么? 可以用来吃的吗?那么这篇文章可能并不适合你。

历史背景

随着 webapp 的复杂程度不断地增加,同时 node 社区的崛起也让前端在除了浏览器之外各方面予以强力衍生与渗透,不得不承认目前的前端开发已然是一个庞大和复杂的体系,慢慢的前端工程化这个概念开始逐渐被强调和重视。

在支付宝,前端工程化应该是先驱者,我们有一套配套工具和脚手架来交代项目的初始,调试与打包;有成熟的线上部署和迭代系统交代每一行代码的始与终;有智能化的监控系统来交代代码的优与劣。

如上这么做好处是显然的,让所有的事情变得可控与规范,但前端开发的灵活与便捷性却带来不少影响,代码不管是否本地经过验证,要发布必须重新全量构建整个项目,而有些同学的把预发调试当成和本地调试一样使用,先不提这种做法是否正确但衍生而来的问题是用户必须忍受每次全量构建的时长,另外也由于项目复杂的增大,通常一个中型的项目业务模块都会有上百个,加上应用架构所包含的内容,已经是一个不小的体量,即使进行单次构建,也可能让开发者足足等上几分钟,甚至几十分钟。

本文的重点就是想要让开发者尽量缩小带薪的痛苦等待。

受益群体

想知道 webpack 优化技巧,想要提升构建速度。

数据报备

本机硬件环境:

2 GHz Intel Core i7 / 4 核
8 GB 1600 MHz DDR3

项目数据:

457 个项目文件,项目依赖 40 个

运行数据:

在 node@4 npm3 下整个全量构建耗时达 192s

时间都去哪儿了

在埋头开始优化前,首先我们必须理清楚知道一次全量构建他所包含的时间分别由什么组成,这可能让我们更加全面的去评估一个问题。

T总 = T下载依赖 + Twebpack

Twebpack = Tloaders + Tplugins

在如上粗略的评估中我们可以把时间归结在两大部分,一个是下载依赖耗时,还有一个是 webpack 构建耗时,而这一部分耗时可能包含了各类 loader 和 plugin 的耗时,css-loader ? babel-loader ? UglifyJsPlugin ?现在我们并不清楚。

如何做

基于如上的评估我们大概可以从四大方面来着手处理

  • 从环境着手,提升下载依赖速度;
  • 从项目自身着手,代码组织是否合理,依赖使用是否合理,反面提升效率;
  • 从 webpack 自身优化手段着手,优化配置,提升 webpack 效率;
  • 从 webpack 可能存在的不足着手,优化不足,进一步提升效率。

从环境出发

一般碰到问题我们最容易想到的是升级一下依赖,把涉及构建的工具在 break change 版本之前都升级到最新,往往能带来意向不到的收益,这和机器出现问题重启往往能解决有异曲同工之处。

而事实上,也确实如此,在该项目中升级项目构建模块后提升了 10s 左右,暂时先不提其中的原由。

另外不得不吐槽 npm3 中安装依赖实在是龟速,那如何压榨安装依赖所需要的时间呢,是否有方案?或许大家已经听到过 pnpm,这里需要介绍下更为好用和极速的 npminstall

先来看看项目在不同版本的 npm 以及在 npmintall 场景下的差异:

node@4.2.4 | tnpm@2 (npm2)

修正后共计耗时 284s

依赖安装时长 182s

构建时长 Time: 102768ms 约为 102s


node@4 | tnpm@3 (npm3)

共计耗时 192s

依赖安装时长 64s

初始构建所需 Time: 128027ms 约为 128s


node@5.8.0 | tnpm@4 (npminstall)

共计耗时 140s

依赖安装时长 15s

初始构建所需 Time: 125271ms 约为 125s


使用 tnpm@4/npminstall 能够达到立竿见影的效果,优化幅度达 70% - 90%

司内

npm install -g tnpm@4 --registry=http://registry.npm.alibaba-inc.com

司外

npm install npminstall --g
npminstall 

从项目自身出发

在未压缩的情况下脚本大于 1MB 变得非常普遍,甚者达到 3-4MB,这到底是因为什么?!

在这个过程中我分析了项目中的依赖,以及代码使用的状况。有几个案例非常的普遍。

案例一: 依赖与依赖从属不明确

package.json

"lodash": "^4.13.1",
"lodash.clonedeep": "^3.0.2",

在这个案例中根据需求只需要保留其一即可

案例二: 废弃依赖没有及时删除

很多时候我们业务变更很快,人员变动也很快,多人协同,这在项目中很容易被窥探出来

import xx from 'xxx';

然后在业务实现中并没有使用 xx

必须及时删除已经停止使用的相关库,无论是 deps 还是 devdeps,都使用 uninstall 的方式把相关依赖从 package.json 中移除,并千万记得也从源码中移除相关依赖。

案例三: 为了实现小功能而引用大型 lib

webpack 强大的混淆能力,让 web 开发 和 node 开发的界线变得模糊,依赖的滥用问题异常凸显。

moment(key).format('YYYY-MM-DD HH:mm:ss')
// 只是使用了 moment 的format 何必引入 moment
// 如果不想简单实现就可以使用更为专一的库来实现
// https://github.com/taylorhakes/fecha Lightweight date formatting and parsing (~2KB). Meant to replace parsing and formatting functionality of moment.js.


import isequal from 'lodash/isequal'
// 这样会导致整个 lodash 都被打入到包内
// 为何不直接使用
// import isequal from 'lodash.isequal'

等等等等。

引入一个第三方 lib 的时候,请再三思量,问自己是否有必要,是否能简单实现,是否可以有更优的 lib 选择

案例四: 忽略三方库的优化插件

这个项目是使用 antd-init 脚手架工具初始化而来的项目,所以在构建工具层面使用了 ant-tool/atool-build. 对于已经熟悉 ant-design 的同学而言,以上这些应该都已熟悉。

在查阅项目代码时候碰到了又一个很典型的案例

webpack.config.js 中引用了优化引用的插件 babel-plugin-antd

webpackConfig.babel.plugins.push(['antd', {
  style: 'css',
}]);

该插件会在 babel 语法解析层面对引用关系梳理即用什么的组件就只会引用什么样组件的代码以及样式。

// import js and css modularly, parsed by babel-plugin-antd
import { DatePicker } from 'antd';

但是很让人忧心的是我们很容易在原始代码里面找到这样的踪迹

import 'antd/lib/index.css';

一个错用可能就会让包的体积大上一个量级。

如上这类优化插件比如在 babel-plugin-lodash 中也有相关实现。

如果三方库有提供优化类插件,那么请合理的使用这类插件,此外之后 atool-build 也会升级到 webpack2,在 webpack2 中已经支持 tree-shaking 特性,那么如上优化插件可能就并不需要了。

案例五:babel-runtime 和 babel-polyfill

由于历史的原因 babel@5 到 babel@6,polyfill 推荐的形式也并不一样。但是在项目中我们可以发现一点是,开发人员并不清楚这其中的原委。以至于代码中我们经常可以看到的一种情形是以下两种方式共存:

//js文件
require('babel-pollyfill');
"dependencies": {
  "babel-runtime": "*"
},
"devDependencies": {
  "babel-plugin-transform-runtime": "*"
},
"babel": {
  "presets": [
    "es2015",
    "stage-0"
  ],
  "plugins": [
    "add-module-exports",
    "transform-runtime"
  ]
}

两种方式只需要一种即可,更加推荐下一种方式,在压缩的情况下至少能给代码减少 50KB 的体积

案例六:css-module

在 atool-build 中默认会对 *.module.less*.module.css 的文件使用 css-module 来处理

atool-build 关于 css-module

而 css-module 这一块的处理由 css-loader 完成。css-loader 关于 css-module

对 css-module 还不清楚的同学可以移步至 阮一峰老师的 blog

简单来说使用 css-module 后可以保证某个组件的样式,不会影响到其他组件

在日常中经常有同学会跑过来问,为什么我的样式变成有 hash 后缀了,为什么构建文件变大了,原因就在于此。

如果你的项目使用 ant-design, 并且通过 antd-init 脚手架来生成项目,那么你所有的 less 文件都会被应用 css-module,代码

这本应是一种好的方式,但是在实际项目中开发者并不清楚其中的逻辑,并且在使用在也不规范,如手动直接调用大型组件的 less 文件的同时也调用其 css 文件。

应用 css-module 后会导致构建的文件体积变大,如果小项目,并且能自己管理好命名空间的情况下可以不开启,反之请开启。

另外关于 css-loader 自从版本 0.14.5 之后压缩耗时增加几十倍的问题,其实之前在本地做过相应的测试,

css-loader 分别尝试过 0.14.5 和 0.23.x

然后并没有在这个业务项目中发现问题,但不保证别的业务项目中会复现这个问题,基于此记录一笔。

官方 issue 地址

案例七 发布至 npm 的包并未 es5 化

目前在 atool-build 中处理 jsx 时并不会像处理 js 一样对 node_modules 目录下的内容进行屏蔽。

而现在在内部项目中可以看到大量的场景借 jsx 核没有 es5 化,这无疑是构建性能中巨大瓶颈的一块。

发布至 npm 的包,请全部 es5 化


综上开源世界的选择很多很精彩,但回过来头来想想我们是不是有点过分的利用了这份便捷,少了些对前端本身的敬畏呢。我们要合理适度的使用三方依赖,并认真思考每一步选择背后所需要承担的结果。

通过依赖的精简,使用上的规范,在构建速度上提升了 12秒,在代码压缩的情况下省下了约 900KB 的空间

从 webpack 自身优化点出发

在蚂蚁内部我们使用 atool-build 来进行前端资源文件的构建,如果并不清楚 atool-build 的同学可以前往 ant-tool/atool-build 文档 来进行了解。

笼统的说 atool-build 是基于 webpack 的构建工具,在其内部内置了一套通用型的 webpack 配置,同时这套配置通过 webpack.config.js 来进行重写。

atool-build 中已经内置优化

在讲 CommonsChunkPlugin 前,可能大家需要理清楚一点是 entry 的概念,entry 在 atool-build 中更多意义上指的是一个页面的入口,这个入口对应一个 html 一个 js 一个 css,(这里并不排除不是这么做的,比如在 OLD IE 中对一个样式文件长度有限制,可能需要人为不得不进行拆分,这个时候借助 entry 或许是一种方式)如果在 multiple page 方案中就会有多个 entry,而 CommonsChunkPlugin 的作用是,在如上这些 entry 涉及的 chunk 中抽取公共部分的 module 合并进入一个 chunk;这里可能很多人有误区是认为抽取公共部分指的是能抽取某个代码片段,其实并不是,它是以 module 为单位的。

举几个典型的案例

现在有 entryA, entryB, entryC 和 entryD。

1)抽取 entryA, entryB, entryC, entryD 中所有公共部分的 modules(该 modules 必须都被所有 entry 所引用到) 进入一个 chunk

webpackConfig.plugins.push(
  new webpack.optimize.CommonsChunkPlugin('common', 'common.js')
)

2)抽取 entryA, entryB, entryC, entryD 中公共部分的 modules(该 modules 必须都被指定个数的 entry 所引用到) 进入一个 chunk

webpackConfig.plugins.push(
  new webpack.optimize.CommonsChunkPlugin({
    name: "commonOfAll",
    minChunks: 3
  })
)

3)只抽取 entryA, entryB 的公共部分

webpackConfig.plugins.push(
  new webpack.optimize.CommonsChunkPlugin({
    name: "commonOfAB",
    chunks: ['entryA', 'entryB']
  })
)

4)把 entry 中一些 lib 抽取到 vendor

首先可以在 entry 中设定一个 entry 名叫 vendor,并把 vendor 设置为所需要的 lib

webpackConfig.entry = {
  vendor: ['jquery']
}

webpackConfig.plugins.push(
  new CommonsChunkPlugin({
    name: "vendor",
    minChunks: Infinity,
  })
)

DedupePlugin 在 atool-build 中的应用 代码定位

这个插件中可以在打包的时候删除重复或者相似的文件,实际测试中应该是文件级别的重复的文件,相似没测出来。这个优化对于还在使用 npm2 的同学会特别有用,因为 npm2 不像 npm3 对整体的依赖进行拍平。

举例

在 npm2 目录结构下直接打包如图依赖关系,并且 d 没有被 a 直接依赖,会出现 d 重复打包问题:

a
|--b
    |--d
|--c
    |--d

该插件的功能是会在 resolve 所有的 module 后,对所有的 module 根据调用的次数重新给模块分配更短的 ids,从而减小最终构建产物的文件大小。该插件在 webpack2 中有类似的默认已经内置。在 atool-build 该插件默认内置。

在 webpack 官方站点 关于构建性能优化还提到了关于

resolve.root vs resolve.modulesDirectories

文中所说

Only use resolve.modulesDirectories for nested paths. Most paths should use resolve.root. This can give significant performance gains. See also this discussion.

atool-build 对其设置

resolve.root 一般情况下指向的是项目的根目录,是一个绝对路径;而 modulesDirectories 则是用以模块解析的目录名,一般情况下是相对路径。

简单来说把 resolve.modulesDirectories 设置为 ["node_modules", "bower_components"]

那么在项目中 foo/bar 的文件下依赖一个模块 a

那么 webpack 会通过如下的顺序去寻找依赖

foo/bar/node_modules/a
foo/bar/bower_components/a
foo/node_modules/a
foo/bower_components/a
node_modules/a
bower_components/a

反观我们的的 atool-build 中相关的设置是有优化空间的

尝试调整如下:

resolve: {
- modulesDirectories: ['node_modules', join(__dirname, '../node_modules')],
+ modulesDirectories: ['node_modules'],
}

调整完毕后,整体构建时长降低了 7s

webpack 提供的优化

第一次接触这个插件来源于在 stackoverflow 上看到的一个问题 how-to-optimize-webpacks-build-time-using-prefetchplugin-analyse-tool

随后翻看了相关的文档,文档中提到可以用来 boost performance 听上去很诱人,但在实际使用中并没有那么顺利。

要使用 PrefetchPlugin 插件,首先需要了解清楚哪些依赖或者模块需要被 prefetch。而这些就需要衍生出 webpack cli 和 其 analyse 工具。

webpack ci json 把构建输出的日志生成到一个 json 文件中,然后通过 analyse 工具分析。

atool-build 目前已经内置了协助分析所需的 stats.json

$ atool-build --json

之后会在构建结果的目标目录会生成一个 build-bundle.json 基于数据安全原因建议大家自己搭建 analyse 平台,上传这个 json 文件查看效果。

上传完毕后,可以在页面最右侧的导航栏中可以看到 hints 这一级。点击之后便可以看到,Long module build chains 而 prefetch 的用意就在于实现 prefetch 一些文件或者模块以缩短 build chain。

举例在该项目中一处冗长构建链:

prefetch

在这个案例中就可以把 babel-runtime 给 prefetch

new webpack.PrefetchPlugin('babel-runtime/core-js')

优化后,构建提速在毫秒级,效果不明显。

结论: preFetch 这类优化需贴合具体的应用场景,所以并不具有普遍性。对于构建速度的性能提升可能都不会明显。它的作用个人觉得是让你发现可能存在的性能问题,并通过 webpack 其他手段,比如之后会提到的 noParse 手段来解决其中的瓶颈

在此非常推荐一款 webpack 的插件,该插件可以让你清楚的看到代码的组成部分,以及在项目中可能存在的多版本引用的问题。

webpack-visualizer-plugin

在 atool-build 中使用只需要在 webpack.config.js 中做对应设置即可

var Visualizer = require('webpack-visualizer-plugin');
module.exports = function(webpackConfig) {
  webpackConfig.plugins.push(new Visualizer());

  return webpackConfig;
}

Visualizer

简单来说 external 就是把我们的依赖申明为一个外部依赖,外部依赖通过 <script> 外链脚本引入。

在 atool-build 使用上

webpack.config.js

module.exports = function(webpackConfig) {
  ...

+ webpackConfig.externals = ['react', 'react-dom', 'react-router', 'classnames', 'immutable', 'g2']

  ...

  return webpackConfig;

};

并在对应的 entry 页面中通过 script 的方式传入这些库 cdn 的地址。

结论:提升整体构建时间 20s 以上,并在压缩代码的情况下省下约 1MB 的空间,个人比较推荐这种方式,因为有更好的 cdn 缓存加持。

  • alias 和 noParse

如上我们已经知道如何声明一个外部依赖并通过 cdn 的方式来优化构建,这种方式是把依赖脱离了整个 bundle,可能有些情况下你需要把这个外部依赖打包进入到你的 bundle 但是你又不想为此而花费很长时间,如何做呢?

webpack.config.js

module.exports = function(webpackConfig) {
  ...

+ webpackConfig.resolve.alias = {
+   'react': 'react/dist/react.min'
+ }
+ webpackConfig.module.noParse.push(
+  /react.min/
+ )
  ...

  return webpackConfig;

};

经过如上设置,那么 require('react'); 等价于于 require('react/dist/react.min.js')

而 noParse 则会让 webpack 忽略对其进行文件的解析,直接会进入最后的 bundle

在项目中合理使用 alias 和 noParse 可以提升效率,但该选择更适合生产环境,否则调试时会比较尴尬

在正常项目中我们会发现除了自身代码外,我们的 deps 中也引用了大量的 npm 包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其分析,如何来规避此类内耗呢?这两个插件就是干这个用的。

简单来说 DllPlugin 的作用是预先编译一些模块,而 DllReferencePlugin 则是把这些预先编译好的模块引用起来。这边需要注意的是 DllPlugin 必须要在 DllReferencePlugin 执行前,执行过一次。

那在 atool-build 中如何使用呢

给项目新增一个 dll.config.js

var join = require('path').join;
var webpack = require('atool-build/lib/webpack');
var pkg = require(join(__dirname, 'package.json'));
var dependencyNames = Object.keys(pkg.dependencies);

var uniq = require('lodash.uniq');
var pullAll = require('lodash.pullall');
var autoprefixer = require('autoprefixer');

var exclude = []

var deps = uniq(dependencyNames);
var entry = pullAll(deps, exclude);
var outputPath = join(process.cwd(), 'dll/');

module.exports = function(webpackConfig, env) {
  var babelQuery = webpackConfig.babel
  webpackConfig = {};

  webpackConfig = {
    context: __dirname,
    entry: {
      vendor: entry
    },
    devtool: 'eval',
    output: {
      filename: '[name].dll.js',
      path: outputPath,
      library: '[name]',
    },
    resolve: {
      modulesDirectories: ['node_modules'],
      extensions: ['', '.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json']
    },
    resolveLoader: {
      modulesDirectories: ['node_modules'],
    },
    module: {

      loaders: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          loader: 'babel',
          query: babelQuery
        },
        {
          test: /\.jsx$/,
          loader: 'babel',
          query: babelQuery
        },
        {
          test: /\.tsx?$/,
          loaders: ['babel', 'ts'],
        },
        {
          test(filePath) {
            return /\.css$/.test(filePath) && !/\.module\.css$/.test(filePath);
          },
          loader: 'css?sourceMap&-restructuring!postcss',
        },
        {
          test: /\.module\.css$/,
          loader: 'css?sourceMap&-restructuring&modules&localIdentName=[local]___[hash:base64:5]!postcss',
        },
        {
          test(filePath) {
            return /\.less$/.test(filePath) && !/\.module\.less$/.test(filePath);
          },
          loader: 'css?sourceMap!' +
            'postcss!' +
            'less-loader?{"sourceMap":true}',
        },
        {
          test: /\.module\.less$/,
          loader: 'css?sourceMap&modules&localIdentName=[local]___[hash:base64:5]!!' +
            'postcss!' +
            'less-loader?{"sourceMap":true}'
        },
        { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=application/font-woff' },
        { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=application/font-woff' },
        { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=application/octet-stream' },
        { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' },
        { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=image/svg+xml' },
        { test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i, loader: 'url?limit=10000' },
        { test: /\.json$/, loader: 'json' },
        { test: /\.html?$/, loader: 'file?name=[name].[ext]' },
      ]
    },
    postcss: [
      autoprefixer({
        browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 8', 'iOS >= 8', 'Android >= 4'],
      }),
    ],
    plugins: [
      new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') })
    ]
  }

  return webpackConfig;
}

如上是把一个项目中的 deps 全部放入了 dll 中,运行如下命令后

atool-build --config dll.config.js

会在项目中生成一个 dll 文件夹其中包含了 vendor.dll.js 和 vendor.json

并在原有 webpack.config.js 中添如下代码片段

if (env === 'development') {
  webpackConfig.plugins.some(function(plugin, i){
    if(plugin instanceof webpack.optimize.CommonsChunkPlugin) {
      webpackConfig.plugins.splice(i, 1);

      return true;
    }
  });
  webpackConfig.plugins.push(
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('./dll/vendor.json')
    })
  )
}

大功告成。

*请注意:dllPlugin 和 commonChunkPlugin 是二选一的,并且在启用 dll 后和 external、common 一样需要在页面中引用对应的脚本,在 dll 中就是需要手动引用 vendor.dll.js *

在实际使用中,dllPlugin 更加倾向于 开发环境,而对开发环境的整体提速非常明显。如下图所示,初次构建的速度优化约 20%,再次构建速度优化为 40%,hot-reload 约为50%

dll

从 webpack 不足出发

在 webpack 中虽然所有的 loader 都会被 async 并发调用,但是从实质上来讲它还是运行在单个 node 的进程中,以及在同一个事件循环中。虽然单进程在处理 IO 效率上要强于 多进程,但是在一些同步并且非常耗 cpu 过程中,多进程应该是优于单进程的,比如现在的项目中会用 babel 来 transform 大量的文件。所以 happypack 的性能提升大概就来源于此。 当然也可以预见到,如果你的项目并不复杂,没有大量的 ast 语法树解析层的事情要做,那么即使用了 happypack 成效基本可视为无。

在我这次尝试的优化项目中,400多模块都需要涉及 babel 加载,并且还存在 npm 包并没有 es5 的情况(bad),所以可以预见到的是会有不错的结果。

webpack.config.js 中添加如下代码片段

var babelQuery = webpackConfig.babel;
var happyThreadPool = HappyPack.ThreadPool({ size: 25 });
function createHappyPlugin(id, loaders) {
  console.log('id', id)
  return new HappyPack({
    id: id,
    loaders: loaders,
    threadPool: happyThreadPool,

    // disable happy caching with HAPPY_CACHE=0
    cache: true,

    // make happy more verbose with HAPPY_VERBOSE=1
    verbose: process.env.HAPPY_VERBOSE === '1',
  });
}
webpackConfig.module = {};

webpackConfig.module = {
  loaders: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'happypack/loader?id=js',
    },
    {
      test: /\.jsx$/,
      loader: 'happypack/loader?id=jsx',
    },
    {
      test(filePath) {
        return /\.css$/.test(filePath) && !/\.module\.css$/.test(filePath);
      },
      loader: ExtractTextPlugin.extract('style', 'happypack/loader?id=cssWithoutModules')
    },
    {
      test: /\.module\.css$/,
      loader: ExtractTextPlugin.extract('style', 'happypack/loader?id=cssWithModules')
    },
    {
      test(filePath) {
        return /\.less$/.test(filePath) && !/\.module\.less$/.test(filePath);
      },
      loader: ExtractTextPlugin.extract('style', 'happypack/loader?id=lessWithoutModules')
    },
    {
      test: /\.module\.less$/,
      loader: ExtractTextPlugin.extract('style', 'happypack/loader?id=lessWithModules')
    }
  ],
}
if (!!handleFontAndImg) {
  webpackConfig.module.loaders.concat([
    { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'happypack/loader?id=woff' },
    { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'happypack/loader?id=woff2' },
    { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'happypack/loader?id=ttf' },
    { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'happypack/loader?id=eot' },
    { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'happypack/loader?id=svg' },
    { test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i, loader: 'happypack/loader?id=img' },
    { test: /\.json$/, loader: 'happypack/loader?id=json' },
    { test: /\.html?$/, loader: 'happypack/loader?id=html' }    
  ])
} else {
  webpackConfig.module.loaders.concat([
    { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=application/font-woff' },
    { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=application/font-woff' },
    { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=application/octet-stream' },
    { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' },
    { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=image/svg+xml' },
    { test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i, loader: 'url?limit=10000' },
    { test: /\.json$/, loader: 'json' },
    { test: /\.html?$/, loader: 'file?name=[name].[ext]' }
  ])
}
webpackConfig.plugins.push(createHappyPlugin('js', ['babel?'+JSON.stringify(babelQuery)]))
webpackConfig.plugins.push(createHappyPlugin('jsx', ['babel?'+JSON.stringify(babelQuery)]))
webpackConfig.plugins.push(createHappyPlugin('cssWithoutModules', ['css?sourceMap&-restructuring!postcss']))
webpackConfig.plugins.push(createHappyPlugin('cssWithModules', ['css?sourceMap&-restructuring&modules&localIdentName=[local]___[hash:base64:5]!postcss']))
webpackConfig.plugins.push(createHappyPlugin('lessWithoutModules', ['css?sourceMap!postcss!less-loader?sourceMap']))
webpackConfig.plugins.push(createHappyPlugin('lessWithModules', ['css?sourceMap&modules&localIdentName=[local]___[hash:base64:5]!postcss!less-loader?sourceMap']))
if (!!handleFontAndImg) {
  webpackConfig.plugins.push(createHappyPlugin('woff', ['url?limit=10000&minetype=application/font-woff']))
  webpackConfig.plugins.push(createHappyPlugin('woff2', ['url?limit=10000&minetype=application/font-woff']))
  webpackConfig.plugins.push(createHappyPlugin('ttf', ['url?limit=10000&minetype=application/octet-stream']))
  webpackConfig.plugins.push(createHappyPlugin('eot', ['file']))
  webpackConfig.plugins.push(createHappyPlugin('svg', ['url?limit=10000&minetype=image/svg+xml']))
  webpackConfig.plugins.push(createHappyPlugin('img', ['url?limit=10000']))
  webpackConfig.plugins.push(createHappyPlugin('json', ['json']))
  webpackConfig.plugins.push(createHappyPlugin('html', ['file?name=[name].[ext]']))
}

happy

总结来说,在需要大量 cpu 计算的场景下,使用 happypack 能给项目带来不少的性能提升。从本次优化项目来看,在建立在 dll 的基础上,初次构建能再提升 40% 以上,重新构建也会提升 40% 以上。所以 dllPlugin 和 happypack 的结合可以大大优化开发环节的时间。

  • uglifyPlugin 慢如蜗牛

uglify 过程应该是整个构建过程中除了 resolve and parse module 外最为耗时的一个环节。之前一直想要尝试在这个过程的优化,最近看社区新闻的时候,发现了 webpack-uglify-parallel 深入看了这个库的组织和实现,完全就是我需要的,因为它就是基于 uglifyPlugin 修改而来。

webpack.config.js 中启用 webpack-uglify-parallel 多核并行压缩

webpackConfig.plugins.some(function(plugin, i) {
  if (plugin instanceof webpack.optimize.UglifyJsPlugin) {
    webpackConfig.plugins.splice(i, 1);
    return true;
  }
});
var os = require('os');
var options = {
  workers: os.cpus().length,
  output: {
    ascii_only: true,
  },
  compress: {
    warnings: false,
  },
  sourceMap: false
}
var UglifyJsParallelPlugin = require('webpack-uglify-parallel');
webpackConfig.plugins.push(
  new UglifyJsParallelPlugin(options)
);

由于在 atool-build 中我们已经内置了 UglifyJsPlugin,所以一开始我们对该插件予以了删除。

parallel

结论:初次构建速度优化至少 40% 本地 8 核心 而服务端达到 20 核心。但是问题是在多核平行压缩中 cpu 负载非常高,如果服务端多项目并发构建,结果可能很难讲。需要有节制使用多核的并行能力。

  • 最后大家可能很好奇,webpack-uglify-parallel 和 happypack 和 external 的混搭会带来怎么样的化学反应。

rock

如上这个组合拳非常适合在生成环节下使用,而 dll + happypack 则更加适合在开发环节。

总结

本地构建时长 从原先的 125s 优化到了 36s,优化幅度为 70% 左右

本地调试时长从原先的 22s 优化到了 7s,优化幅度也为 70% 左右

webpack-uglify-parallel 和 happypack 和 external 的混搭非常适合在生产环节

dll + happypack 的混搭非常适合在开发环节

@jaredleechn
Copy link

👍

@soda-x
Copy link
Owner Author

soda-x commented Aug 29, 2016

atool-build 计划在 1.0 版本中支持 webpack@2

调研 webpack-uglify-parallel 在 webpack@2 中的可行性,可行直接取代 现有UglifyPlugin

在开发环节中 dora 内置 dll 逻辑 boost dev performance

external noParse 等常规优化由用户业务决定,自行配置

happypack 是否需要内置到 atool-build 需要做进一步讨论,个人认为 happypack 接管 babel-loader 即可,而选择是否启用 happypack 则通过 atool-build 参数的形式来决定是否 boost build performance.

@soda-x soda-x changed the title Boost webpack build performance | webpack 构建性能探索 Boost webpack build performance | Optimising webpack build performance | Webpack 构建性能优化探索 Aug 30, 2016
@qiuyuntao
Copy link

👍

@scarletsky
Copy link

写得太好拉! 👍

@orange7986
Copy link

orange7986 commented Dec 1, 2016

nice! 👍

@taikongfeizhu
Copy link

写的非常棒,其实dll相对于external还有两个好处,一是解决类似react-addons类似的这种一句话话框架:module.exports = require('react/lib/ReactCSSTransitionGroup');等重复打包和引用问题,二是减少exeternal的反复定义,另外一个大项目资源下不同的app之间共用一些基础资源如react+redux+react-router+immutable全家桶之类的资源,dll可以提前编译出结果供不同的配置资源来使用。

@soda-x soda-x added the webpack label Jan 4, 2017
@Tankpt
Copy link

Tankpt commented Jan 5, 2017

问一个问题。关于resolve. modulesDirectories 我的理解应该是一个相对的路径。然后工具里面还有一个是

 join(__dirname, '../node_modules')]

这个解析出来的不是一个绝对路径吗?

@soda-x
Copy link
Owner Author

soda-x commented Jan 5, 2017

嗯 这边这么处理的原因在于 基于 atool-build 做封装的

@Tankpt
Copy link

Tankpt commented Jan 5, 2017

那如果按照你之前给的例子的写法

 modulesDirectories: ['node_modules', join(__dirname, '../node_modules')],
+ modulesDirectories: ['node_modules'],

那查找的顺序应该是什么样子的呢?

@soda-x
Copy link
Owner Author

soda-x commented Jan 5, 2017

ant-tool/atool-build#213 供参考其中的原因. 顺序无非优先级的变更.

@maoziliang
Copy link

babel-runtime 和 babel-polyfill还是不一样的,babel-runtime对于Array.prototype.find这种对象方法,没法做到自动polyfill,只会处理静态方法。

@creeperyang
Copy link

@maoziliang 👍

NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing builtins (Use babel-polyfill for that). https://babeljs.io/docs/plugins/transform-runtime/

案例五:babel-runtime 和 babel-polyfill 建议可以点一下。

@paranoidjk
Copy link

@pigcan
这种配置方式不对吧?还是在内部保存了常用库的一个map?

+ webpackConfig.externals = ['react', 'react-dom', 'react-router', 'classnames', 'immutable', 'g2']

@soda-x
Copy link
Owner Author

soda-x commented Feb 15, 2017

@maoziliang 确实是这样的 感谢指出.

@soda-x
Copy link
Owner Author

soda-x commented Feb 15, 2017

@paranoidjk 这里引用了我们的基于 webpack 的构建工具 atool-build

@yup9
Copy link

yup9 commented Mar 15, 2017

@pigcan 想问一下为什么说DllPlugin 更适合于开发环境,用于生产环境会有什么弊端吗?

@nimoc
Copy link

nimoc commented Mar 15, 2017

@yup9
生成环境中如果你在迭代的过程中新增了一个第三方模块, dll 的文件就需要用户重新下载。

如果是不用 dll 发布生产环境代码,新增第三方后只有依赖第三方的文件需要用户重新下载

@yup9
Copy link

yup9 commented Mar 15, 2017

@nimojs 但是如果使用externals (将多个第三方依赖打包成一个文件)也会有这个问题,新增了一个依赖,也需要重新下载整个 externals文件。

@nimoc
Copy link

nimoc commented Mar 15, 2017

@yup9
对也会有,但一般 externals 不会放所有第三方组件,比如我们项目基本就只放 react react-dom.

另外,有一种 增量更新 的方法,每次迭代只上传编译后的新增代码。但我没有深入研究过。

@MarksJin
Copy link

我想把dll打的包,直接放到cdn上,然后入口html引入,这样做不是可以在生产上用dll吗?而且dll要比externals好点

@nimoc
Copy link

nimoc commented Aug 17, 2017

@MarksJin 一般来说dll 会包含所有 npm 的代码,dll 文件会非常大。如果产品迭代 dll 新增了包,那么用户需要重新下载很大的包。

当然这是根据业务场景决定的。
如果你将 dll 打包的方式引入到正式环境,能很好的利用缓存。那是可以的。


但是大部分情况下 dll 是用于解决本地开发阶段编译速度慢的问题。


@soda-x
Copy link
Owner Author

soda-x commented Aug 18, 2017

@tylerrrkd
Copy link

感受到旧项目被atool-build支配的感觉。

@soda-x
Copy link
Owner Author

soda-x commented Jun 27, 2019

@tylerrrkd atool 还是比较好升级上来的

@tylerrrkd
Copy link

@pigcan 啊哈 开了分支在搞了 嘿嘿

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