Webpack优化指南

webpack基本知识咱就不多说了。关于webpack优化,网上有很多文章,介绍了各种各样的优化方法。我随机挑了几篇看了看,大部分文章优化围绕的中心思想都是如何提升编译速度。但右三觉得,webpack优化不仅仅是要让webpack编译越来越快,而应该包含以下3个目标:

  • 提高编译速度
  • 减小编译输出的文件体积
  • 帮助提升页面性能

提高编译速度

如果你的项目有前端构建,随着项目越来越大,“编译慢”迟早会成为让人头疼的问题。想想这个场景,你修改代码,准备看页面效果,但却要等上5s甚至更长时间。前端开发的同学大多是多显示器的,一边写代码,一边即时预览效果。但因为编译慢的原因,这种无缝衔接的工作方式被打破了。这不但影响工作效率,更影响心情,工作被打断,等等等的滋味可不好受。这也是我们追求更快编译速度的根本原因。

要提升编译速度,在项目大小,机器确定的前提下,大概的优化方向也是3个(grunt/gulp同理):

  • 增量编译
  • 加缓存
  • 任务并行

我们一起来看看,在这3个方向上,如何提升webpack的编译速度

webpack与增量编译

webpack watch 默认就是增量编译,我们就不需要再额外做什么了

webpack与缓存

1. 开启webpack缓存

module.exports = {
  cache: true,
  // ... 其他配置
}

2. 开启loader缓存

module: {
  rules: [
    {
      test: /\.js$/,
      loader: ['babel-loader?cacheDirectory=true'] // 开启babel-loader缓存
    }
  ]
}

3. 使用模块缓存

hard-source-webpack-plugin这个插件对开发时编译速度的提升是巨大的,比开启其它缓存(webpack缓存或loader缓存)效果好太多。我手上一个项目,没优化前watch编译一次大约要5s,加上这个插件后,直接降到2s左右,强烈推荐这个插件

webpack与并行

happypack插件为webpack增加并行处理的能力。也强烈建议使用,如果项目基于vue要注意vue-loader目前还不支持happypack

优化webpack模块查询

除了上面列到的方法,优化webpack模块查询,从而减少webpack自身执行开销,对提升编译速度也有一定帮助。具体做法是:

  • resolve.alias,设置别名
  • resolve.modules,指定模块
  • loader.include && loader.exclude,精细指定loader处理的文件
  • resolve.extensions,设置模块查找时的自动补全扩展名。如果项目没用到jsx, json,就不要写上了

减少需要编译的模块

1. webpack.DllPlugin + webpack.DllRefrencePlugin

每个项目都会有一些基础依赖库,如果能把这些依赖提前编译好,这就减少了webpack要处理的模块,自然,也是可以提升编译速度的。webpack.DllPlugin可以把依赖打包成dll库,供其它模块使用。

let config = {
    resolve: {
        extensions: ['.js', '.vue'],
        alias: {
            '@@': path.resolve(__dirname, '../node_modules'),
        }
    },
    entry: {
        // 把vue全家桶,axios等打包成dll库
        webDll: [
            '@@/vue/dist/vue.esm.js',
            '@@/vue-router',
            '@@/vuex',
            '@@/axios',
            '@@/urijs'
        ]
    }
    ,
    output: {
        path: path.join(__dirname, dir),
        filename: '[name].js',
        library: '[name]_[hash]'
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.join(__dirname, dir, '[name]-manifest.json'),
            name: '[name]_[hash]',
            context: __dirname
        }),
    ],
    module: baseConfig.module
}
// 引用 dll-manifest.json文件
new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require(`./${manifestDir}/webDll-manifest.json`)
})

2. module.noParse

指定不需要解析的模块

3. externals

排除外部依赖,不需要对其进行处理

减小编译输出的文件体积

到目前,上面的内容主要是围绕开发(development)场景进行的,方便我们高效无缝地进行开发。但相比开发环境,发布到线上的前端资源就更重要了。因为这才是项目真正使用的资源文件,所以对生产环境(production)的前端资源文件进行webpack优化也是必不可少的。

在生产环境,我们的优化原则就是文件越小越好,越少越好。一般来说,页面需要加载的文件体积越小,加载的文件个数越少,页面的性能就越可能更好(这里的页面性能主要指首屏时间和页面可交互时间。这里为什么加“可能”两个字,可以看第三部分);同时,更小更少的文件,就意味着更小更少的带宽和流量。CDN可是要花钱的,这也是直接帮公司省钱呢。

那么如何让我们的文件能越来越小呢?答案很简单:同一个模块只打包一份,文件大小就是最小的。这时相信大家都想到了webpack.optimize.CommonsChunkPlugin插件,也都会在webpack中加上类似如下的配置:

new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: 2
})

CommonsChunkPlugin插件会帮我们把重复的模块提取到vendor这个chunk中。但如果大家仔细研究过webpack最终打包的dist文件,就会发现,有一些重复引用多次的文件并没有被提取到vendor中,而是在多个chunk中被重复打包了。这里推荐使用webpack官方分析平台,上传stats.json后,在hints里面会有一些打包建议,基本上都是重复打包的信息。我们的目标就是要把hints的建议全解决了。

到于如何得到stats.json文件,可以直接使用命令参数 --json,也可以通过代码的方式生成:

var webpack = require('webpack/lib/webpack.js')
var options = require('./build/webpack.conf')
var compiler = webpack(options)

compiler.run(function (err, stats) {
  if (err) throw err
  // 生成stats.json文件
  fs.writeFile(path.resolve(__dirname, 'webpack.stats.json'), JSON.stringify(stats.toJson(), null, ' '), (err) => {
      if (err) throw err
  })
})

那么为什么有些多次重复引用的模块没有被提到vendor中呢。原因是不同chunk间的多次引用不算重复引用 System.import, require.ensure等方法会生成新的chunk,这些chunk间多次重复引用同一模块,并不会被提到vendor中。在下面例子中,index.js,detail.js,about.js都依赖了模块m.js,但m.js会在每个chunk(System.import)中都存在,并不是我们相像的那样,被单独提取出来了:

// main.js
import Index from './index'
import Detail from './detail'
import About from './about'

const Index = System.import('./index')
const Detail = System.import('./detail')
const About = System.import('./about')

require.include

要想m.js被单独出来,可以使用require.include,把指定模块加入到特定chunk中。下面例子中的m.js就会在main.js所在的chunk中了,而不会在重复存在于index,detail,about这3个chunk:

// main.js
import Index from './index'
import Detail from './detail'
import About from './about'

require.include('./m')
const Index = System.import('./index')
const Detail = System.import('./detail')
const About = System.import('./about')

异步公共模块

虽然require.include可以在多个异步chunk中解决依赖模块被重复打包的问题,但在实际使用上,还是有一些不足。如果公共依赖模块m.js很多,这个时候到入口chunk中去一个一个加require.include显然不现实;更重要的是,到入口chunk中去加require.include这本身就不灵活,因为打包入口chunk是可以任意指定的。

webpack当然考虑到了这个问题。解决办法就是使用webpack.optimize.CommonsChunkPlugin的异步公共模块:

new webpack.optimize.CommonsChunkPlugin({
  async: true,
  minChunks: function (module, count) {
    // 强制指定把m.js打包成一个异步依赖模块
    return module.resource && module.resource.indexOf('m.js') > -1
  },
})

通过上面的配置,webpack也会把异步chunk中(各chunk中虽然只引用了一次m.js)的m.js独立打包成一个异步依赖,在加载主chunk时会先加载m.js依赖。这样也保证了m.js在打包时只打包了一份。

帮助提升页面性能

webpack帮助我们打包生成对应的dist文件,那么怎样生成的dist文件是最有助于提升页面性能呢?web性能优化原则中与文件相关的规则有以下一些:

  • 使用HTTP缓存
  • 预加载
  • 按需加载
  • 首屏CSS内嵌
  • DNS预拉取
  • ...

所以,如何结合业务自身情况,合理地配置chunk进行代码分片,从而充分利用缓存,更合理地做预加载,按需加载等,这些都是可以结合webpack,不断调优的。

小结

webpack现在已经成为前端构建标配,本文主要围绕提高编译速度、减小编译输出的文件体积、帮助提升页面性能三个方面阐述了右三对webpack优化的理解,希望对读者有些许帮助。

留言列表
  • 222:
    2222
      2018年04月07日 11:59 回复
    • test:
      this is a test comment!
      • 11111:
        111111
        2018年04月07日 11:54
      • replay to test:
        replay to test !
        2018年04月07日 10:47
      2018年04月06日 19:50 回复

    发表评论: