快一点, 再快一点

1,300 阅读4分钟

话说,最近项目的开发环境升级,引入了 js 构建工具 webpack,生产力大大提高,happy 了两个月后,随着项目代码逐日增多,我们遇到了一个问题,构建太慢了。慢到什么程度呢,开发时第一次启动要40秒左右,生产构建则需要1分多钟甚至2分钟。看着似乎不是特别慢ho,你可以试着出了 bug 后和 pm, 测试,设计小姐姐注视2分钟…

人生苦短 // 我用 python,一刻千金。大好时光怎么可以把时间浪费在构建上呢?so,快一点,再快一点!

分析

我们是多页面应用,拥有很多入口,构建工具需要去遍历指定文件夹内的指定 js 文件并生成对应的 html 文件出来,然后我们有 rename 的需求,即生成的 html 的文件名可以和 js 文件名不一致。
于是我们在 js 里提供了一个特定语句:

// card/index.js
@Entry({
    filename: 'card_list.html'
})
`

然后在编译时通过 babel parse 出 ast 后拿到 filename 的值,再交给 webpack 。
这样的好处是,开发时能够直观的设置文件名称,后续维护的时候也很直观的知道最终输出的文件名称。时间上的消耗在项目初期代码不是很多,js 文件不是很大的时候也还是可以接受的。但当 js 文件越来越多,越来越大,就有点让人头疼了。而且为了满足就近原则,我们会把公用的业务逻辑抽出来放在同文件夹内,就会浪费很多时间在这些非 entry 文件上。

结论

所以结论就是:ast 好用,但是有点花时间。

加速

秉着谁拖后腿就干掉谁的原则,首先 babel parse to ast 这个我们就别要了,找个其他能满足我们需求的办法,有没有呢?当然有,而且就近在眼前。用过 gulp 的同学肯定都见过这行代码:gulp.src('client/templates/*.jade')gulp api,这里的匹配规则使用的node-glob。glob 在匹配特定文件时非常方便,且支持ignore,用来匹配我们的 entry 实在太合适不过了。下面是获取 entries 的例子:

function getEntries(dir = 'src/pages') {
    const entries = {}
    const root = resolve(dir) + '/'
    const files = glob.sync(root + '**/*.js', {
    ignore: [
        '**/view.js',
        '**/_*/**', 
        '_*.js'
    ]
    })
    files.forEach((file) => {
    entries[
        file
        .replace(root, '')
        .replace('/index.js', '')
        .replace(/\//g, '_')
        .replace('.js', '')
    ] = file
    })
    return entries
}

这里我们通过特定的类似 sass 中的做法约定非 entry 文件使用_前缀以忽略。并根据文件的路径生成 {dir_filename: absolutePath} 这样的 entries 给 webpack。同时,为了满足 rename 的需求,允许提供一个 router.js 文件来定义需要 rename 的 entry,类似:

// router.js
module.exports = {
  rename: require.resolve(filePath)
}

然后通过下面的方法对原始 entry 和 router 进行 merge:

function mergeEntries(entry, router) {
    const reducer = (prev, [key, val]) => {
    prev[val] = key
    return prev
    }
    const reverse = source => Object
    .entries(source)
    .reduce(reducer, {})
    return reverse(reverse(Object.assign({}, entry, router)))
}

然后把最终的 entries 交给 webpack 就 ok 了。

再快一点

原来的 webpack 使用的是 v1.x 版本,最近 webpack 已经发布了3.x 的版本,更加的 powerful。于是我便将依赖也升级到最新的 v3.5.5,然后傻眼了。在多 entry 的情况下,webpack3 与最新的 html-webpack-plugin 配合起来并不默契。。会导致构建时间几何翻倍,详细看可以看这个issue
在这个 issue 里我发现了个有意思的东西,就是 html-webpack-plugin 的 v1.7.0 版本简直不要太快,降级到 v1.7.0 后构建速度果然飞起来了,比起 2.x 都要快很多。但是它不支持chunksSortMode: dependency,意味者当你在生产构建时使用了CommonsChunkPlugin后将无法正确的排列 js 的引入顺序。不过好在它支持 function 参数,且我们的 chunks 是固定的,便可以通过下面这样的方式使用:

Object.keys(config.entry).map((file) => {
    const chunks = ['manifest', 'vendor', 'commons', file]
    return new HtmlWebpackPlugin({
        chunks,
        filename: file + '.html',
        template: resolve('index.html'),
        inject: 'head',
        minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        },
        chunksSortMode: (a, b) => chunks.indexOf(a.names[0]) - chunks.indexOf(b.names[0])
    })
})

到这里,其实速度已经可以了,但不容易满足的我岂会到这里就结束。
之前曾在淘宝 FED 看过一篇讲happypack的文章,happypack 是 webpack 的一个插件,目的是通过多进程模型,来加速代码构建,具体原理可以看这里
happpack 的使用方法很简单:

// webpack config
const HappyPack = require('happypack')
const os = require('os')
const config = {
    module: {
    rules: [
        {
        test: /\.js$/,
        loader: 'happypack/loader?id=js'
        }
    ]
    },
    plugins: [
    new HappyPack({
        id: 'js',
        // 创建一个线程池共享给所有的 HappyPack 实例
        threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
        loaders: [{
        path: 'babel-loader',
        query: {
            cacheDirectory: true
        }
        }]
    })
    ]
}

至此为止,本次对构建速度的优化就结束了,效果如何呢?开发时 1st 构建可以控制在 10s 左右,生产构建在 uglify + gzip 的情况下 1st 构建 8s,有 cache 的情况下改动单个文件 3.5s。可以说是开上飞机啦!

结语

其实 webpack 还有一些优化空间,比如 dllPlugin,不过目前的速度我已经很满足了😎,等到遇到下一个瓶颈的时候再去研究吧。如果你有更好的优化方法,欢迎分享评论。