关于webpack优化,你需要知道的事(上篇)

2,331

前言

webpack 是一个优秀的打包工具,其本身为我们做了大量优化,同时也为我们提供了大量的配置项让我们可以自定义,从而有优化空间。

在讲 webpack 优化篇之前,由于楼主主要以 vue 脚手架开始的,而且是已经升级为 webpack4 之后的优化,如果对 vue脚手架配置不太了解的同学。可以看我上一篇文章 如何优雅的升级到webpack4,或者直接看 webpack3 vue脚手架注解

下面我先讲讲vue脚手架为我们做的一些优化,不喜欢看的请跳过,然后会讲如何在优化的基础上升华一下,内容从浅到深,但是所有的方法都经过楼主考证,内容较长,请自带板凳瓜子。

vue-cli 脚手架自带优化

babel

Babel 是一个 JavaScript 编译器,能将 ES6 代码转为 ES5 代码,让你使用最新的语言特性而不用担心兼容性问题,并且可以通过插件机制根据需求灵活的扩展。这里我不讲babel ,而是讲官方用的插件 transform-runtime,对应的插件全名叫做 babel-plugin-transform-runtime,其作用是减少冗余代码,到底是怎么减少的呢?

例如在转换 class extent 语法时会在转换后的 ES5 代码里注入 _extent 辅助函数用于实现继承:

function _extent(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i];
    for (var key in source) {
      if (Object.prototype.hasOwnProperty.call(source, key)) {
        target[key] = source[key];
      }
    }
  }
  return target;
}

这会导致每个使用了 class extent 语法的文件都被注入重复的_extent 辅助函数代码,babel-plugin-transform-runtime 的作用在于不把辅助函数内容注入到文件里,而是注入一条导入语句:

var _extent = require('babel-runtime/helpers/_extent');

这样能减小 Babel 编译出来的代码的文件大小。 注意:babel-plugin-transform-runtime 必须和 babel-runtime 需要配套使用

说来惭愧,楼主试了一下,把这个插件去掉,生成文件的hash和大小并没有变化(汗,别砸,翻资料webpack 标准入门前端工程化-webpack篇之babel-polyfill与babel-runtime(三)上有写,而且脚手架上有)。后来发现,楼主的代码中并没有es6。后来换了一个大项目,做了对比

babel对比
可以发现,图右边是去掉插件的。体积明显大了一点。使用此插件可以减少重复代码,缩小项目体积。

缩小文件搜索范围

loader

使用 Loader 时可以通过 test 、 include 、 exclude 三个配置项来命中,对于我们的项目大部分都是 js,下面看看官方脚手架 js 的 babel-loader:

module.exports = {
    // ...
    module: {
        rules: [
            // ...
           {
                test: /\.js$/,
                loader: 'babel-loader',
                include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
           },
        ]
    }
}

由于通过 npm 安装的第三方的库,都是经过 webpack 打包 es5 化了,所以这里就可以只对 include 包括的文件使用 babel-loader 解析

注意了。由于 css、less 的引入是要插入到js中的,所以并不适用于这个(把 node_modules 排除在外)方法。说到这里,多说一句,也是曾经很困扰我的 css 的 loader 解析顺序,use 的 loader 解析顺序跟数组的位置是反着的,以 less 为例,具体来讲

module.exports = {
    // ...
    module: {
        rules: [
            // ...
           {
                test: /\.less$/,
                // less 文件的处理顺序为先 less-loader 再 css-loader 再 vue-style-loader
                use: [
                    // style-loader 会把 CSS 代码转换成字符串后,注入到 JavaScript 代码中去,
                    'vue-style-loader',
                    // css-loader 会找出 CSS 代码中的 @import 和 url() 这样的导入语句,告诉 Webpack 依赖这些资源。同时还支持 CSS Modules、压缩 CSS 等功能。处理完后再把结果交给 vue-style-loader 去处理。
                    {
                        loader: 'css-loader',
                        options: {
                            sourceMap: config.dev.cssSourceMap
                        }
                    },
                    //通过 less-loader 把 less 源码转换为 CSS 代码,再把 CSS 代码交给 css-loader 去处理。
                    {
                        loader: 'less-loader'
                    }
                ] 
            },
        ]
    }
}

关于缩小范围增加命中这个思想,还可以做很多事情,这里只讲了vue脚手架优化做的事情,更多配置请往后看,看我如何自定义的

node 选项

webpack 的官方脚手架里面的node选项可以防止node包,还有 setImmediate 的 profill注入到代码中

node: {
    // prevent webpack from injecting useless setImmediate polyfill because Vue
    // source contains it (although only uses it if it's native).
    setImmediate: false,
    // prevent webpack from injecting mocks to Node native modules
    // that does not make sense for the client
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
}

好不好,看疗效。那么具体的疗效怎么样呢,楼主同样的代码,做了对比,效果如下:

node选项对比

通过对比可以看到,两次打包css的hash值全部变了,js部分hash发生改变(这个打包没看出js变化,但是另一个项目的部分js的hash变了)。总体打出来的包的体积相差不大。去掉node选项打包时间差别不明显,所以用不用,见仁见智吧。我看create-react-app中也使用了,所以还是建议使用吧,知道更多的,可以留言区讨论。

js、css 压缩

css 压缩这个就不多说了,大家都懂,

值得一提的是由于 UglifyJsPlugin 插件升级到1.0之后有了 parallel选项,开启了多线程压缩

new UglifyJsPlugin({
  uglifyOptions: {
    compress: {
      warnings: false
    }
  },
  sourceMap: config.build.productionSourceMap,
  parallel: true  // 开启多线程压缩
})

这两个插件都有配置项,合理配置可以优化项目。后面会讲。

代码分割

代码分割就是将动态引入的代码分割成一个一个的代码块(chunk),根据需求加载到html上。注意:要使用代码分割功能,在项目中要配合使用组件、路由懒加载的方式(可以通过import实现)

webpack4 的 mode 为 production 时,默认会对代码进行分割。楼主看了 webpack3 的代码分割方式是使用 CommonsChunkPlugin 插件,目的就是分割出几类代码:

  1. vendor 也就是第三方库打包这里。
  2. manifest 当编译器开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest"
  3. app 这个是代码中的公共部分

HashedModuleIdsPlugin

嗯 webpack 生成 js 的 hash 是如何计算我并不清楚,但是如果不用这个插件的话,所有生成 js 的 hash 是一样的,而且只要有一点点改动,所有文件的 hash 值都会变化。那造成什么样的结果呢?

比如你只改了 b 页面的 js 里的一行代码,如果不用此插件的话,所有页面的 js 的 hash 全部会变化,浏览器要重新请求全部的js。性能浪费到令人发指。而使用了 HashedModuleIdsPlugin 这个插件,只有你改动的那个 chunk 的 hash会发生变化,其他不变,由于浏览器的缓存机制,浏览器只重新请求改动的js。是不是很棒。而且上一小节对代码分割那里的分割方式,也是为了把不经常变动的文件单独打包,hash 可以保持不变。

使用方法也很简单

new webpack.HashedModuleIdsPlugin(),

什么?为什么就算去掉 HashedModuleIdsPlugin 插件 用脚手架第一次打包项目生成的 js 的 hash 不全部一样,而且改动之后,也不是全部发生变化啊。这个也是楼主遇到的问题。楼主不用脚手架搭建的项目,js 的 hash 是一样的,知道为什么出现初始打包的 js hash 值为什么不全部一样的同学,欢迎评论区讨论。

作用域提升(scope hoisting)

过去 webpack 打包时的一个取舍是将 bundle 中各个模块单独打包成闭包。这些打包函数使你的 JavaScript 在浏览器中处理的更慢。相比之下,一些工具像 Closure Compiler 和 RollupJS 可以提升(hoist)或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。 个插件会在 webpack 中实现以上的预编译功能。

new webpack.optimize.ModuleConcatenationPlugin()

这种连结行为被称为“作用域提升(scope hoisting)

记住,此插件仅适用于由 webpack 直接处理的 ES6 模块。在使用转译器(transpiler)时,你需要禁用对模块的处理(例如 Babel 中的 modules 选项)。

css 优化

由于css加载不会阻塞dom的解析,所以把css抽取出来。不占用js的大小是一个明智的选择 OptimizeCSSPlugin 插件做的就是这个,并且代码复用,会减小css体积

new OptimizeCSSPlugin({
  cssProcessorOptions: config.build.productionSourceMap
    ? { safe: true, map: { inline: false } }
    : { safe: true }
}),

总结

总体来讲 webpack 为我们做的优化有

  1. babel-plugin-transform-runtime 插件去除重复垫片代码
  2. module.rules 的 js 解析,使用 include 提高命中
  3. node 选项,防止 node 的自带包(dgram、fs、net、tls、child_process)注入到我们的代码中
  4. js、css 压缩,代码分割,公共部分抽离
  5. 维持打包后不变chunk的hash值不变
  6. 作用域提升(scope hoisting)
  7. css 抽离。公共部分抽离

大致就这样了,有没有讲到的也请评论区提出,那么如何在此基础上做优化呢,这个也许是大家都很关心的问题。接下来我会在 《关于webpack优化,你需要知道的事(下篇)》讲到。