Webpack构建优化——压缩代码

压缩代码

浏览器从服务器访问网页时获取的JavaScript、CSS资源都是文本形式的,文件越大网页加载时间越长。 为了提升网页加速速度和减少网络传输流量,可以对这些资源进行压缩。 压缩的方法除了可以通过GZIP算法对文件压缩外,还可以对文本本身进行压缩。
对文本本身进行压缩的作用除了有提升网页加载速度的优势外,还具有混淆源码的作用。由于压缩后的代码可读性非常差,就算别人下载到了网页的代码,也大大增加了代码分析和改造的难度。

压缩JavaScript

目前最成熟的JavaScript代码压缩工具是UglifyJS,它会分析JavaScript代码语法树,理解代码含义,从而能做到诸如去掉无效代码、去掉日志输出代码、缩短变量名等优化。
要在Webpack中接入UglifyJS需要通过插件的形式,目前有两个成熟的插件,分别是:

  • UglifyJsPlugin:通过封装 UglifyJS 实现压缩。
  • ParallelUglifyPlugin:多进程并行处理压缩。

UglifyJS提供了非常多的选择用于配置在压缩过程中采用哪些规则。 由于选项非常多,就挑出一些常用的拿出来详细讲解其应用方式:

  • sourceMap:是否为压缩后的代码生成对应的Source Map,默认为不生成,开启后耗时会大大增加。一般不会把压缩后的代码的Source Map发送给网站用户的浏览器,而是用于内部开发人员调试线上代码时使用。
  • beautify: 是否输出可读性较强的代码,即会保留空格和制表符,默认为是,为了达到更好的压缩效果,可以设置为false
  • comments:是否保留代码中的注释,默认为保留,为了达到更好的压缩效果,可以设置为false
  • compress.warnings:是否在UglifyJs删除没有用到的代码时输出警告信息,默认为输出,可以设置为 false 以关闭这些作用不大的警告。
  • drop_console:是否剔除代码中所有的console语句,默认为不剔除。开启后不仅可以提升代码压缩效果,也可以兼容不支持console语句IE浏览器。
  • collapse_vars:是否内嵌定义了但是只用到一次的变量,例如把var x = 5; y = x转换成y = 5,默认为不转换。为了达到更好的压缩效果,可以设置为true
  • reduce_vars: 是否提取出出现多次但是没有定义成变量去引用的静态值,例如把x = 'Hello'; y = 'Hello' 转换成var a = 'Hello'; x = a; y = b,默认为不转换。为了达到更好的压缩效果,可以设置为true

也就是说,在不影响代码正确执行的前提下,最优化的代码压缩配置为如下:

const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
module.exports = {
  plugins: [
    // 压缩输出的 JS 代码
    new UglifyJSPlugin({
      compress: {
        // 在UglifyJs删除没有用到的代码时不输出警告
        warnings: false,
        // 删除所有的 `console` 语句,可以兼容ie浏览器
        drop_console: true,
        // 内嵌定义了但是只用到一次的变量
        collapse_vars: true,
        // 提取出出现多次但是没有定义成变量去引用的静态值
        reduce_vars: true,
      },
      output: {
        // 最紧凑的输出
        beautify: false,
        // 删除所有的注释
        comments: false,
      }
    }),
  ],
};

除此之外Webpack还提供了一个更简便的方法来接入UglifyJSPlugin,直接在启动Webpack时带上--optimize-minimize参数,即webpack --optimize-minimize, 这样Webpack会自动为你注入一个带有默认配置的UglifyJSPlugin。

压缩ES6

虽然当前大多数JavaScript引擎还不完全支持ES6中的新特性,但在一些特定的运行环境下已经可以直接执行ES6代码了,例如最新版的Chrome、ReactNative的引擎JavaScriptCore。
运行ES6的代码相比于转换后的ES5代码有如下优点:

  • 一样的逻辑用ES6实现的代码量比ES5更少。
  • JavaScript引擎对ES6中的语法做了性能优化,例如针对const申明的变量有更快的读取速度。

所以在运行环境允许的情况下,我们要尽可能的使用原生的ES6代码去运行,而不是转换后的ES5代码。
在你用上面所讲的压缩方法去压缩ES6代码时,你会发现UglifyJS会报错退出,原因是UglifyJS只认识ES5语法的代码。 为了压缩ES6代码,需要使用专门针对ES6代码的UglifyES。
UglifyES和UglifyJS来自同一个项目的不同分支,它们的配置项基本相同,只是接入Webpack时有所区别。 在给Webpack接入UglifyES时,不能使用内置的UglifyJsPlugin,而是需要单独安装和使用最新版本的uglifyjs-webpack-plugin。安装方法如下:

npm i -D uglifyjs-webpack-plugin@beta

Webpack 相关配置代码如下:

const UglifyESPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
  plugins: [
    new UglifyESPlugin({
      // 多嵌套了一层
      uglifyOptions: {
        compress: {
          // 在UglifyJs删除没有用到的代码时不输出警告
          warnings: false,
          // 删除所有的 `console` 语句,可以兼容ie浏览器
          drop_console: true,
          // 内嵌定义了但是只用到一次的变量
          collapse_vars: true,
          // 提取出出现多次但是没有定义成变量去引用的静态值
          reduce_vars: true,
        },
        output: {
          // 最紧凑的输出
          beautify: false,
          // 删除所有的注释
          comments: false,
        }
      }
    })
  ]
}

同时,为了不让babel-loader输出ES5语法的代码,需要去掉.babelrc配置文件中的babel-preset-env,但是其它的Babel插件,比如babel-preset-react还是要保留, 因为正是babel-preset-env负责把ES6代码转换为ES5代码。

压缩CSS

CSS代码也可以像JavaScript那样被压缩,以达到提升加载速度和代码混淆的作用。 目前比较成熟可靠的CSS压缩工具是cssnano,基于PostCSS。
cssnano能理解CSS代码的含义,而不仅仅是删掉空格,例如:

  • margin: 10px 20px 10px 20px被压缩成margin: 10px 20px
  • color: #ff0000被压缩成color:red

cssnano接入到Webpack中也非常简单,因为css-loader已经将其内置了,要开启cssnano去压缩代码只需要开启css-loaderminimize选项。 相关Webpack配置如下:

const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,// 增加对 CSS 文件的支持
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          // 通过 minimize 选项压缩 CSS 代码
          use: ['css-loader?minimize']
        }),
      },
    ]
  },
  plugins: [
    // 用 WebPlugin 生成对应的 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路径
      filename: 'index.html' // 输出的 HTML 的文件名称
    }),
    new ExtractTextPlugin({
      filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 Hash 值
    }),
  ],
};

ParallelUglifyPlugin

用过UglifyJS的你一定会发现在构建用于开发环境的代码时很快就能完成,但在构建用于线上的代码时构建一直卡在一个时间点迟迟没有反应,其实卡住的这个时候就是在进行代码压缩。
由于压缩JavaScript代码需要先把代码解析成用Object抽象表示的AST语法树,再去应用各种规则分析和处理AST,导致这个过程计算量巨大,耗时非常多。
当Webpack有多个JavaScript文件需要输出和压缩时,原本会使用UglifyJS去一个个挨着压缩再输出, 但是ParallelUglifyPlugin则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UglifyJS去压缩代码,但是变成了并行执行。 所以ParallelUglifyPlugin能更快的完成对多个文件的压缩工作。
使用ParallelUglifyPlugin也非常简单,把原来Webpack配置文件中内置的UglifyJsPlugin去掉后,再替换成ParallelUglifyPlugin,相关代码如下:

const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  plugins: [
    // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS 的参数
      uglifyJS: {
        output: {
          // 最紧凑的输出
          beautify: false,
          // 删除所有的注释
          comments: false,
        },
        compress: {
          // 在UglifyJs删除没有用到的代码时不输出警告
          warnings: false,
          // 删除所有的 `console` 语句,可以兼容ie浏览器
          drop_console: true,
          // 内嵌定义了但是只用到一次的变量
          collapse_vars: true,
          // 提取出出现多次但是没有定义成变量去引用的静态值
          reduce_vars: true,
        }
      },
    }),
  ],
};

在通过new ParallelUglifyPlugin()实例化时,支持以下参数:

  • test:使用正则去匹配哪些文件需要被ParallelUglifyPlugin压缩,默认是/.js$/,也就是默认压缩所有的.js文件。
  • include:使用正则去命中需要被ParallelUglifyPlugin压缩的文件。默认为[]
  • exclude:使用正则去命中不需要被ParallelUglifyPlugin压缩的文件。默认为[]
  • cacheDir:缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回。cacheDir用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。
  • workerCount:开启几个子进程去并发的执行压缩。默认是当前运行电脑的CPU核数减去1。
  • sourceMap:是否输出Source Map,这会导致压缩过程变慢。
  • uglifyJS:用于压缩ES5代码时的配置,Object类型,直接透传给UglifyJS的参数。
  • uglifyES:用于压缩ES6代码时的配置,Object类型,直接透传给UglifyES的参数。

其中的testincludeexclude与配置Loader时的思想和用法一样。
UglifyES是UglifyJS的变种,专门用于压缩ES6代码,它们两都出自于同一个项目,并且它们两不能同时使用。
UglifyES一般用于给比较新的JavaScript 运行环境压缩代码,例如用于ReactNative的代码运行在兼容性较好的JavaScriptCore引擎中,为了得到更好的性能和尺寸,采用UglifyES压缩效果会更好。
ParallelUglifyPlugin同时内置了UglifyJS和UglifyES,也就是说ParallelUglifyPlugin支持并行压缩ES6代码。

接入ParallelUglifyPlugin后,项目需要安装新的依赖:

npm i -D webpack-parallel-uglify-plugin

安装成功后,重新执行构建你会发现速度变快了许多。如果设置cacheDir开启了缓存,在之后的构建中会变的更快。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容