webpack 构建优化

从去年 6 月份到现在开发的两个项目,都有围绕 webpack 进行构建打包。在使用 webpack 上也有一定的心得。在过去一周,将 webpack 版本升级为 2,同时又针对项目进行了一定的优化。接下来就跟大家做一个简单的分享(本文的 webpack 配置代码是基于 webpack 2.2版本)。

为何使用 webpack ?

在使用一个项目构建工具之前,我们首先要想一想项目构建的目标是什么?不妨从以下几点思考一下:

  1. 是否满足构建需求
    自动化构建主要解决的问题就是自动化,比如:代码检查,文件合并,资源压缩,脚本编译等等。显然 webpack 有丰富的 loader 和 plugin,并且主要定位是一个模块化的构建方案,基本可以满足大部分的构建需求。

  2. 构建成本和扩展性
    webpack 本身的目标还是解决模块化构建的问题,提供一个方便快捷的配置型模块化构建方案。如果你的项目是多页面,并且有精细化资源处理策略。使用 webpack 显然需要有较为复杂的配置文件。并且可能随着项目资源入口增加或者打包处理策略的多样化,webpack 可能存在一定的瓶颈。比如:一个多页面的项目,我们根据页面内容或者分类,需要进行多样的针对性的脚本编译和文件合并方案。显然 webpack 不是一个很好的解决方案。

  3. 工作流
    任何工具或者技术脱离业务场景和项目现状来谈,往往缺乏说服力。构建工具除了解决基本的自动化问题,当面临团队多人协作开发和前端工程化的问题,选择一个合适团队的构建方案,提供一个高效,可维护的工作流才是重中之重。基于 webpack,gulp,grunt 等工具进行二次封装,来集成环境依赖,资源管理,发布流程等任务的工作流可以大大提高我们的工作效率。

当然以上三点仅是一个参考,并不是说 webpack 已经解决了这几个问题,亦或者说 webpack 是最好的方案,还是需要符合个人或者团队的现状。本文也不会完全解决这些问题。只是一个实践的分享。

webpack 构建

在进行 webpack 构建的过程,也是一个渐进实现的我们构建目标的过程,不要期望一次就能完美的配置好。我们需要合理的分阶段的完成我们的目标。首先是可以保证我们的项目通过 webpack 进行构建是可运行,可用的。这一部分作为基础就不在这篇文章中介绍了,我们着重针对优化来进行讲解。

优化目标

优化目标(优化什么?)是我们进行优化首先要确定的。目前根据我们团队的问题和大家对 webpack 构建的吐槽。我们注重围绕两个方面进行优化:

  1. 合理构建项目资源:包括如果缩小资源打包大小,异步加载资源等等。
  2. 缩短项目构建时间:包括热更新(hot reload)和生产环境打包(build)两个方面。

接下来我们就针对不同目标,来进行针对性的优化。

合理构建项目资源

合理构建项目资源就是围绕着,减少资源打包体积,优化客户端资源加载等展开
###合理拆包
在项目中,我们常用的 js 资源主要包括:基础类库,第三方工具库,公共UI组件库,业务UI组件,业务组件等等中的几种组合。合理拆包可以优化打包速度,同时也可以提高资源加载速度。但是拆包要依据一定规则。

###异步资源加载
在进行单页面开发时,如果一次加载所有模块的代码,将会给首屏渲染带来很大的负担。尤其在移动端开发,网络环境比较复杂,异步延迟加载低频非主页内容是一个很好的解决方案。使用方式如下:

// 异步加载方式
require.ensure([], (require) => {
  require('../pageA.js').default
})

上边仅仅是使用的方式,最简单直接就是和 react-router 结合,使用 getComponet 的来加载子页面。如下:

const routes = {
  // ...
  {
    path: 'pageA',
    getComponent(nextState, cb) {
      require.ensure([], (require) => {
        cb(null, require('./views/pageA').default)
        // ...
      })
    }
  },
  // ....
}

当然具体还要和项目本身诉求和子页面加载频率和缓存策略等相关,并不能一律使用懒加载,如果经常加载几 kb 的 js 资源,反而适得其反。

###webpack Tree-shaking
说到 Tree-shaking (摇树)就不得不提到 rollup.js,在 js 中较早引入这个概念并且得到比较好的推广的就是 rollup.js(如果了解 angular2 的应该对这个不陌生)。 webpack2 现在也支持了这个优化技术。两者都是基于 ES6 modules,通过静态分析来进行 tree shaking 的。那我们怎么使用 three shaking 呢?不需要像 Axel 这篇文章Tree-shaking with webpack 2 and Babel6配置,可以直接将babel-preset-es2015的 modules 配置一下即可:

{
  "presets": [
    "es2015",
    {
    "modules": false
    }
  ]
}

另外需要注意的是如果你想直接对 npm 安装的第三方包也能使用 tree-shaking ,就要注意首先第三方库本身是否是使用 ES6 module ,其次要注意 package.jsonmian 的路径指向的是不是经过打包处理后的库资源。但是很多库会按照 rollup 约定使用 jsnext:main 字段。 但是 webpack2 不默认支持这种引入方式,所以需要手动指定。

###构建分析
完成项目构建之后,我们也想了解项目打包后的依赖和打包后的资源内容包含了什么?这时候就可以借助一个工具 webpack-bundle-analyzerimage

缩短项目构建时间

如何缩短项目构建的时间,除了需要进行打包构建的资源本身的大小和处理方式以外,根据不同的环境,需要进行一定的优化和取舍。

###增量更新
增量更新是建立在拆包的基础上的,如果打包资源没有进行拆分,每次局部的修改 webpack 都会重新 rebuild 整个资源。所以基础就是合理拆包,减小更新影响的范围。在此基础上,还要考虑 webpack 是否如我们所期望的,能够提供最优的更新机制。显然现实并没有那么美好。我们还是需要去解决一些问题。 例如:需要使用 dll 或者 commonChunksPlugin 中 names 字段增加 manifest。来避免局部更新影响公共(vendor.js) 文件的重新编译。

###开启缓存
在对资源进行编译合并打包,不论是使用什么 loader 和 plugin,根据需要开启相应的缓存可以大大减少 rebuild 所需时间。比如:使用 babel-loader 通过配置 query 的 cacheDirectory 字段为 true 来开启缓存;happyPack plugin 设置 cache,等等有很多 loader 和 plugin 都提供了缓存支持。

###happypack
happypack 是 webpack 的一个插件,目的是通过多进程模型,来加速代码构建,一般情况可以对 ES6,sass 等编译开启,效果比较明显。但是在 webpack2 上支持不是很友好,但是经过简单的配置,依然可以开启多进程,配置如下:

const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: 4 }); // 创建4个线程的自定义线程池,用于多个插件来共享
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        loaders: ['happypack/loader?id=js'] // id=js
      },
      {
        test: /\.(css|scss|sass)$/,
        // 类似 ExtractTextPlugin,也是通过 loader 中声明来调用 HappyPack 的 plugin
        loader: ExtractTextPlugin.extract({
          fallbackLoader: 'style-loader',
          loader: ['happypack/loader?id=sass']
        })
      }
    ]
  },
  plugins: [
    // babe-loader 进行多线程构建
    new HappyPack({
      cache: true,  // 开启缓存,默认在项目根目录生成.happypack 缓存文件,可以自行配置缓存文件
      cacheContext: {
        env: process.env.NODE_ENV
      },
      id: 'js',  // 通过 id名称进行关联
      threadPool: happyThreadPool, //使用第三方线程池
      loaders: [{
        loader: 'babel-loader',
        query: {
          cacheDirectory: true
        }
      }]
    }),
    // sass-loader 进行多线程构建
    new HappyPack({
      cache: true,
      cacheContext: {
        env: process.env.NODE_ENV
      },
      id: 'sass',
      threadPool: happyThreadPool,
      loaders: [
        'style-loader',
        'css-loader?sourceMap',
        'sass-loader?sourceMap'
      ]
    })
    // ...
  ]
}

经过开启多线程,一般构建时间可以减少 10-30% ,效果还是比较明显的。更多地详细配置可以戳 happypack

###hot reload 构建配置
针对开发环境 hot reload 时,我们一些线上的打包策略和任务很多都可以考虑去除,如: uglify 代码压缩,合理选择 devtool的模式,如果有对图片和字体使用 url-loader 改为 file-loader,不用进行limit限制(不过没有多大的优化),等等。

其它

问题

在项目构建的尝试中,不断地深入过程中也发现了几个问题,在这里和大家分享一下:

###多页面异步 ExtractTextPlugin 问题
在进行多页面打包,并且采用了异步加载的方案后,当我们在多个页面中引用一些公共业务组件的时候,就会出现 bug,具体依赖关系如下图(红色为异步加载模块): image 当进行打包构建后,真实的结果是,引用的 10.js 的页面没有加载 detail-component.css 。原因是因为:extractPlugin 默认不去处理异步 chunk 它会爬取在entry中的各个chunk(非异步的),将包含 css 的chunk中内容移除(此时这个 chunk 内容已被清空),单独打包为 css 。而 10.js 就 import 空的 chunk ,所以就导致样式没有加载成功。
暂时的解决办法就是:1. 业务 UI 组件也剥离出来; 2. 在页面的同步 chunk 直接加载。

###内联 css image 路径报错
当我们在开发环境进行 hot reload 调试,css 文件都被编译放在内存中,以 blob 格式存储。浏览器加载样式文件的协议就是 blob 协议。此时如果 image 没有被打包存储为 blob 格式。我们 css 文件中 url('www.a.com/assets/image.png') 根据 blob 协议在内存中查找,此时无法找到对应的 image 文件。
解决办法:1. 完整路径(包括协议);2.开发环境统一样式,图片和资源的打包方式。

其实还有各种各样的问题和不完美,就不一一提出了。

由于涉及到公司的项目,具体的优化细节如:代码和相关图片,这里就不放出来了。


总结

如果仅仅是配置完成一个简单的 SPA ,使用 webpack 确实经过几句配置就可以搞定。但是整体而言它还有几点不足:

以上都仅仅是我个人的一些见解,可能存在了解不够深入产生的误解或问题。也希望大家能够理解。(是时候开评论了,要不看官们无法吐槽呀)

目录