webpack学习记录(6)-体积优化策略

1,517 阅读5分钟

前言

在之前的webpack学习内容中,介绍了使用webpage打包项目的基础构建,其中讲解了对js、css文件的压缩。这其实可以视为项目体积优化的一部分,下面针对体积优化进行进一步的学习。

注:以下功能都是基于Webpack版本^4.43.0调试。

量化分析

在基础构建中,我们会将项目代码和第三方代码合并打包,统一输出一个bundle文件中。我们若需要对该文件进行优化,思路应该是对其内的模块进行删减、合并(重复内容)。

那么,首先我们需要知道bundle文件各个模板的大小情况,这里需要使用插件webpack-bundle-analyzer,其会输出交互式可视化树图表示bundle文件的大小。

注:也可以使用webpack --profile --json > stats.json命令(不推荐该方案),在生成的stats.json中查看bundle文件的大小,也可以在官方分析工具中进行分析

// webpack.prod.config.js
const baseWebpackConfig = require('./webpack.base.config');

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const webpackConfig = merge(baseWebpackConfig, {
  ...,
  plugins: [
    new BundleAnalyzerPlugin(),
    ...,
  ],
})

module.exports = webpackConfig;

这样,当我们执行npm run build命令后,会打开http://127.0.0.1:8888/:

这样,我们就能很直观的分析出哪个文件的体积最大,其中又是哪个模块的体积最大。

文件压缩

当我们需要优化体积处理时,最初想到的一般是对文件进行压缩。在项目中除了使用gzip来进行压缩文件外,Webpage中也可以对js、css、html文件进行压缩。(可以参考之前的文章开发过程中的基础构建

  • js文件,设置mode的值为production,Webpage会自动压缩
  • css文件,使用插件CssMinimizerWebpackPlugin
  • html文件,设置mode的值为production,Webpage会自动压缩
  • 图片,使用插件image-webpack-loader自动压缩图片

代码拆分

CDN

在实际项目中,我们可以发现在最后生成的bundle文件中,第三方库所占的比重很大。我们可以通过配置externals,将其中常用的第三库(比如vue、vue-router、vuex等)抽离出来,放置在CDN中,通过<script>来引入,减少打包文件体积。

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous">
</script>
// webpack.prod.config.js
module.exports = {
  ...,
  externals: {
    // jquery,即key为项目逻辑代码中引入的第三方库名称
    // jQuery,即value,表示通过script引入之后的全局变量
    jquery: 'jQuery'
  }
};

// 项目逻辑代码
import $ from 'jquery';

...

防止重复

SplitChunksPlugin插件可以将公共的依赖模块提取到已有的 entry chunk 中,或者提取到一个新生成的 chunk。

webpack 将会基于以下条件自动分割代码块:

  • 新的代码块被共享或者来自 node_modules 文件夹
  • 新的代码块大于 30kb (在 min+giz 之前)
  • 按需加载代码块的请求数量应该 <=5

即加载 chunk 所发送的请求不能超过 5 个。

举个例子: a.js 依赖了 1.js、2.js、3.js、4.js 这四个文件,且这四个文件满足了代码分割的前两个条件 (被共享且大于 30kb)。这个时候就会分割出五个 chunk,分别是:a.chunk.js、1.chunk.js、2.chunk.js、3.chunk.js、4.chunk.js。此时加载 chunk~a 的并发请求数就恰好是 5 个。

复制代码如果 a.js 又依赖了 5.js,,这个时候并不会分割出 5.chunk,因为如果分割出 5.chunk,那么加载 a.chunk 的请求就是 6 个了, 5.js 的内容会被合并到 a.chunk 中。

  • 页面初始化时加载代码块的请求数量应该 <=3

    这里的逻辑和上面类型,只不过是针对entry的。

并且,官方在optimization下的splitChunks下提供了默认配置(具体配置说明可以查看这里):

// webpack.config.js
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 这表明将选择哪些块进行优化。 "initial" | "all"(推荐) | "async" (默认) | 函数
      minSize: 30000, // 生成块的最小大小(以字节为单位)
      maxSize: 0,
      minChunks: 1, // 拆分前必须共享模块的最小块数
      maxAsyncRequests: 5, // 按需加载时并行请求的最大数量
      maxInitialRequests: 3, // 入口页面的最大并行请求数
      automaticNameDelimiter: '~', // 默认情况下,webpack将使用块的来源和名称生成名称(例如vendors~main.js)。此选项使您可以指定用于生成名称的定界符。
      name: true, // 拆分块的名称。boolean: true | function (module, chunks, cacheGroupKey) | string
      cacheGroups: { // 这里开始设置缓存的 chunks 缓存组,splitChunks就是根据cacheGroups去拆分模块的
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

动态导入

当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。

这里我们使用第一种方案动态引入。其引入方法为:

import(/** webpackChunkName: "lodash" **/ 'lodash').then(_ => {
 // doSomething
})

接下来,我们还需要修改下webpack配置,添加chunkFilename

// webpack.config.js
module.exports = {
    output: {
      filename: '[name].bundle.js',
+     chunkFilename: '[name].bundle.js', // [name]就是你在import时webpackChunkName的值
      path: path.resolve(__dirname, 'dist')
    },
  };

其他优化

tree-shaking

如果使用ES6的import 语法,那么在生产环境下,会自动移除没有使用到的代码。

// utils.js
const add = (a, b) => {
    return a + b;
}

const minus = (a, b) => {
    return a - b;
}

export {
    add,
    minus
}

//index.js
import {add, minus} from './utils';
add(1, 2);

在项目中,minus不会被构建到bundle中。

scope hosting 作用域提升

JavaScrct中有函数提升和变量提升,会把函数和变量声明提升到当前作用域的顶部。webpack中scope hosting也是类似,将模块进行提升。

scope hosting会分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,这样减少了引入模块的开销。但前提是不能造成代码冗余,因此只有那些被引用了一次的模块才能被合并。

// webpack.config.js
module.exports = {
    plugins: [new webpack.optimize.ModuleConcatenationPlugin()],
  };

由于 Scope Hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。

动态 Polyfill 服务

babel只负责语法转换,比如将ES6的语法转换成ES5。但如果有些对象、方法,浏览器本身不支持,比如:

  • 全局对象:Promise、WeakMap 等。
  • 全局静态函数:Array.from、Object.assign 等。
  • 实例方法:比如 Array.prototype.includes 等。

此时,需要引入babel-polyfill来模拟实现这些对象、方法,一般也称为垫片。

babel-polyfill由于是一次性全部导入整个polyfill,所以导致文件很大,所以我们使用动态 Polyfill 服务进行优化。

每次打开页面,浏览器都会向Polyfill Service发送请求,Polyfill Service识别 User Agent,下发不同的 Polyfill,做到按需加载Polyfill的效果。

参考