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

webpack学习笔记(11)-你为了提升打包的速度,做过哪些尝试? #28

Closed
sevenCon opened this issue Nov 2, 2019 · 0 comments
Labels

Comments

@sevenCon
Copy link
Owner

sevenCon commented Nov 2, 2019

前言

webpack是个很好很好的工具, 前端作为面对资源比较多的一个工种, 为了整理和对接各种各样的资源, 样式资源,脚本资源, 超文本标记语言html,svg, 图片展示资源, 字体等等, 提升用户的体验, 提升开发效率, 对前端的开发流程的痛点, 进行了各种工具的聚合和利用, webpack确实在这方面是前端一个标志性的工具,

webpack里面的许多功能是社区共同努力的结果, 但是为了我们苦逼的前端程序员 的开发,调试,部署提供了太多太多的便利了.

webpack目前进行到了webpack5.0的版本开发, 但是在HMR, Live Reload的的响应时间仍然是不够快.下面总结了一下怎么提高打包效率和减小的打包文件的体积的方法.

以下优化的配置webpack的版本在4.0以上.

提升webpack优化打包速度

babel-loader的cacheDirectory

利用babel-loader我们无负担的使用ES的新特性, 但是webpack的使用,在开发过程中需要不断的重复编译,非常消耗cpu, 又要进行各种loader的处理, 所以如果有缓存, 这会大大减少不必要的重复劳动, cacheDirector就是这么一个属性,可以缓存babel的编译结果.

class A {
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }
  sum() {
    return this.a + this.b;
  }
}

let a = new A(120, 2);
console.log(a.sum());

第一次打包和二次打包的对比图

cacheDirectory的缓存内容

cache的内容是babel-loader的需要polyfill的各种库函数,比如上面需要支持Class的语法, 那么就会缓存进来, 当然缓存的结果是是根据babel的配置而生成的, 使用@babel/polyfill@babel/plugin-transform-runtime生成的库函数当然是会不一样, 详情请看我之前关于babel的文章webpack 学习笔记(7)-bable 的各种依赖理解,corejs, babel-runtime, babel-polyfill 等等-2019-10-13.

这个文件能否应用缓存是有条件的, cacheDiretory的启用会去当前项目的目录下面寻找node_modules/.cache/babel-loader下寻找, 如果找不到则会全局环境下的node_modules/.cache/babel-loader下寻找.

此外, 如果想要缓存失效的话, 可以修改cacheIdentifier的值以强制更新缓存. 默认由@babel/core的版本, babelrc的信息, 环境变量等等生成的值, 如果这些信息改变, 那么也会更新缓存, 当然如果缓存对应的源文件有改动, 缓存也会重新生成, 所以, 一般我们使用的话, 直接开启, 如{cacheDirectory:true}或者'babel-loader?cacheDirector'就可以.

webpack的dllPlugin

这也是一个利用缓存,去减少重复编译的例子, dll简称动态链接库(Dynamic Link Library), 最常见到, 应该是在微软的系统里面, 安装完一个程序之后打开安装的文件夹会看看许多的xxx.dll的文件, 这就是动态链接库. 这是用来给应用程序用的.

来到webpack的编译, 就可以利用这个dllPlugin这个插件, 帮我们缓存已经加载的库, 这些库的文件, 比如vue.js, lodash等等, 不会轻易变动. 自然也就是不需要重复编译.

添加dll 动态链接库之前

添加dll 动态链接库之后

$$5287ms => 3913ms$$

效果是很理想的, 如果想改进打包速度, 一定不要忘记了这个.

loader配置具体的include,exclude

loader的加载, 特别是babel-loader一定不要忘记添加exclude剔除node_modules的打包目录.

{
    test: /.js$/,
    exclude: /(node_modules)/,
    use: {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true
      }
    }
}

一般来说, 上npm的资源都会通过严格的浏览器的兼容测试, 否则也不是一个能够在生成环境使用的包,如果没有兼容, 这个时候就需要对该库的选择进行仔细的考量了, 所以在一般情况下不需要使用babel-loader进行二次的兼容转译的, 除非不得已的情况.

resolve.modules

webpack是用node来实现的打包项目, 其中对于模块的引入, 是commonjs2的规范的, 第三方依赖模块会在安装node_modules下, 而对于require('vue') 这样的语句, node会在当前的目录下去找node_modules, 然后逐级向上一个目录寻找, 最后会到全局的node_modules目录寻找, 项目的层级越深对于模块的寻址负担越大.

通过指定resolve.modules的属性,可以一定程度上减少模块寻址带来的压力

resolve: {
    modules: [ 
        path.resolve('node_modules') 
        // 指定node_modules所在绝对位置, 不需要递归寻址
    ]
}

resolve.mainFields

当我们引入一个第三方模块的时候, 首先会去取当前项目目录node_module上去寻找, 查找并且分析模块的package.json文件, 然后加载main属性对应的文件模块.

在下面配置里面, webpack的mainFields属性, 这样如果导入模块import foo from "foo"; 的时候会先去mainFields里面寻找foo这个模块, 然后在package.json里面寻找brower字段对应的文件, 如果没有再依次的寻找module,main.

{
    resolve:{
        mainFields: ['browser', 'module', 'main']
    }
}

resolve.alias

我们平时配置别名的方式, 会大大的减少我们引入模块的路径书写长度.
比如

resolve:{
    alias:{
        src: path.resolve('src'),        // 匹配前缀    
        fn$: resolve('src/utils/fn.js')  // 精准匹配
    }
}
import fn from "src/utils/fn.js";
// or
import fn from "fn";

resolve.extensions

默认的后缀, 导入模块后缀缺省时, 会在extensions的列表里面, 按照顺序去寻找后缀的文件, 这个后缀列表需要遍历, 所以原则上越少越好,常用的类型放前面.

备注:最好我们引入的模块都加上后缀

resolve: {
    extensions: ['.js', '.vue']
}

module.noParse

针对某些模块不使用loader进行解析.

这个属性是用来忽略特定的模块的, 一些模块不需要经过webpack的模块引入也可以运行,比如jquery, 这样子可以减少webpack的编译时间.

module:{
    rules:[],
    noParse:function(moduleName){
        return /jquery/.test(moduleName)
    }
}

module.externals

通过声明外部模块的别名, 在引入该模块的时候,webpack并不会去node_modules里面去寻找,而是去当前的全局变量中加载, 所以配合externals的属性使用, 需要在模板里面引入对应的script地址, 通常是使用cdn的地址.

一般在生产环境使用.

<script
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"></script>
// webpack.config.js
resolve:{
    externals:{
        jquery: 'jQuery'
    }
}

Happypack 多进程打包

首先明确的是Happypack是对loader的处理, 由主进程分配loader, 打包完成之后合并到主进程进行处理.

所以, 打包的效果提升是有限的, 在充分使用了Dll和babel的cache情况下, 提升并没有说太明显, 但是, 如果系统设计到大量的图片资源base64的处理这种业务, loader处理耗费CPU, 那可能happypack的收益会很高,值得去尝试.

第一步安装:

yarn add happypack

第二步loader的加载资源指定happypack:

module:{
    rules:[{
        test: /\.js/,
        use: ['happypack/loader?id=js']
    }]
},
plugins:[
    new HappyPack({
      id: 'js',
      loaders: ['babel-loader?cacheDirectory']
    }),
]

id是一个标识符,表示当前的资源使用指定的happypack实例,可以同时new 多个happypack进行打包,另外当前的haapypack不支持vue-loader, 如果想要使用happlypack编译vue文件,需要把vue文件里面的内容, 比如js部分, 单独使用happypack来打包.

像这样

{
  test: /\.vue$/,
  include:  src,
  exclude: /node_modules/,
  use: [
    {
      loader: 'vue-loader',
      options: {
        loaders: {
          css: xxx,
          less: xxx,
          js: 'happypack/loader?id=js' 
          // 单独使用id为js的happypack 实例进行打包
        },
     }
    }
  ]
}

在webpack版本迭代优化之后, happypack可能会被遗弃, 毕竟现在webpack的性能也在慢慢变好, 而且多进程有了基于web worker的thread-loader.

web worker 在使用上可能会有一些限制, 比如web workder的进程不能范文webpack 的配置, 不能生成文件不能使用自定义的loader等等.另外一个web worker的生成开销也有大约600ms,使用需要慎重.

ParallelUgligyPlugin 或者 terset-webpack-plugin

多入口的项目或者文件比较多的项目, 推荐使用ParallelUgligyPlugin 或者 terset-webpack-plugin, 都是支持多进程打包的, 而ParallelUgligyPlugin出现的时间比较早, 所以在webpack4.0之后推荐用terset-webpack-plugin, 基本上ParallelUgligyPlugin有的功能都有, 而且是官方的包, 比较稳定.

ParallelUgligyPlugin的使用

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

optimization: {
    minimizer: [
        new ParallelUglifyPlugin({
            cacheDir: '.cache/',
            uglifyJS: {
                compress: {
                    drop_console: true,
                    collapse_vars: true,
                    reduce_vars: true
                }
            }
        }),
    ]
}
  • test, 可选参数, 默认是 /.js$/, 匹配压缩的对象
  • include,exclude, 可选的参数,
  • cacheDir, 可选的缓存目录, 绝对路径
  • workerCount,开启的进程数, 默认cpu的数量-1
  • sourceMap, 默认不开启, 会降低打包的速度
  • uglifyJS, uglifyJs的配置
  • uglifyES, uglifyES的配置信息, 这是一个支持ES Module版本的uglifyJs, 但是也是因为这个库不再维护, 所以webpack默认用terser来进行压,terser也是uglifyJs的一个fork,配置上大同小异.
const TerserPlugin = require('terser-webpack-plugin');
// webpack 4.0 新增的terser-webpack-plugin插件, 可以支持sourcemap, parallel并行压缩, 开启缓存等提高效率的功能
optimization: {
    minimizer: [
        new TerserPlugin({
            cache: true,
            parallel: true,
            sourceMap: false,
            // Must be set to true if using source-maps in production
            terserOptions: {}
        })
    ]
}

## 小结
以上是优化和提升打包速度的几个主要的方式

减低打包的文件体积

Scope Hoisting

这个特性, 会在webpack4.0 配置mode:'production'里面自动开启, 具体的作用是提升作用域, 简单的来说就是把内嵌的函数提升到根路径, 在模块化编程里面, 常见各种文件互相嵌套, 并且是深层次的嵌套, 提升作用域可以减少函数作用域的数量.

{
    mode:'production'
}

更多的scope hoising信息请看这里webpack学习笔记(9)-scope Hoisting, 介绍怎么利用了ModuleConcatenationPlugin插件进行堆栈的抽取和生成的.

Tree Shaking

webpack2.0初步引入,旧版的webapck2.0对tree-shaking的dead code代码剔除支持不是很好, webpack4.0之后默认改用了terser-webpack-plugin进行压缩代码,同时指定package.json#sideEffects属性, 可以有效的剔除dead code.

{
    sideEffects:["*.cs","*.less","*.scss"]
    // or
    sideEffects:false
}

任何import 的代码都有可能产生tree-shaking,包括css,less,scss 比如import '@/less/a.less',如果不指定代码具有副作用, 会在production的时候被剔除.

注意: 或者你也可以使用webpack-css-treeshaking-plugin进行css的tree-shaking,首先是思路没有问题, 但是实践起来, 可能很难, 动态加载的css和class, 很难准确进行判定, 而且作者已经很久没有维护了, 使用会有风险, star很少, 用的人不多, 评测也没有做过,只能算是一个实验性的产品. 在本人的项目运用过程中, 并没有使用, 这里提出来是作为优化的一个思考方向.

生成gzip的压缩文件

在nginx的系统上配置sendfile:on, 开启linux上的系统调用, 可以非常快速的把磁盘的文件推给客户端,避免了读取磁盘, 写入buffer, 打包压缩gzip的过程
.

const CompressionWebpackPlugin = require('compression-webpack-plugin');
// webpack.config.js
plugins:[
    new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: new RegExp('\\.(' + config.build.productionGzipExtensions.join('|') + ')$'),
      threshold: 10240,
      minRatio: 0.8
    })
]

总结

在最后,这篇文件林林总总参考和阅读了许多博客文章和笔记,官方文档,有的在实际项目中实践过,有切身感受, 有的也没有亲身实践,只草草的写了demo实践经验浅薄, 比如happypack, 所以侧漏之处各位不妨提醒,共同学习进步, 感谢!.

参考

https://webpack.js.org/plugins/terser-webpack-plugin/#root
https://louiszhai.github.io/2019/01/04/webpack4/
https://docs.npmjs.com/files/package.json#browser
https://juejin.im/post/5a4dca1d518825128654fa78

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

1 participant