跳至主要內容

webpack优化

Emilia Zhen大约 7 分钟nodejswebpack

production 模块打包自带优化

  1. tree shaking 打包时移除JS中未引用的代码,依赖于importexport的静态结构特性
  2. scope hoisting 将模块之间的关系进行结果推测,让打包出来的代码文件更小、运行更快
  3. 代码压缩 所有代码使用UglifyJsPlugin插件进行压缩、混淆

CSS 优化

mini-css-extract-plugin是用于将CSS提取为独立的文件的插件,对每个包含CSSjs文件都会创建一个CSS文件,支持按需加载CSSsourceMap。异步加载,不重复编译,只针对CSS 安装 -D

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
new MiniCssExtractPlugin({
      filename: '[name].css'
    })
    ····
module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader,'css-loader']
      },
      {
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader,'css-loader','sass-loader']
      },
    ],
  },

自动添加 CSS 前缀

安装postcss-loader autoprefixer

rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader,'css-loader','postcss-loader']
      },
      {
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader,'css-loader','postcss-loader','sass-loader']
      },
    ],

在根目录新建postcss.config.js

module.exports = {
  plugins: [require('autoprefixer')],
}

css 压缩

安装optimize-css-assets-webpack-plugin terser-webpack-plugin

const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
...

optimization: {
  minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},

js 代码分离

代码分离可以用于获取更小的bundle,以及控制资源加载优先级

  1. 使用entry配置手动分离代码
  2. 使用SplitChunksPlugin去重和分离chunk
optimization: {
  splitChunks: {
    chunks: 'all'
  }
}
  1. 动态导入,使用模块的内联函数调用来分离代码,用到那个模块才会加载哪个模块,可以提高SPA首屏加载速度 webpack4默认允许import语法动态导入,但需要babel插件支持 安装@babel/plugin-syntax-dynamic-import,并加入.bablelrc配置文件添加改插件
function getComponent() {
  return import('jquery').then(({ default: $ }) => {
    return $('<div></div>').html('main')
  })
}
window.onload = function () {
  document.getElementById('btn').onclick = function () {
    getComponent().then((res) => res.appendTo('body'))
  }
}

splitChunksPlugin 默认配置

  optimization: {
    splitChunks: {
      chunks: 'async', // 只对异步加载的模块进行拆分,可选值还有all | initial
      minSize: 30000, // 模块最少大于30KB才拆分
      maxSize: 0,  // 模块大小无上限,只要大于30KB都拆分
      minChunks: 1, // 模块最少引用一次才会被拆分
      maxAsyncRequests: 5, // 异步加载时同时发送的请求数量最大不能超过5,超过5的部分不拆分
      maxInitialRequests: 3, // 页面初始化时同时发送的请求数量最大不能超过3,超过3的部分不拆分
      automaticNameDelimiter: '~', // 默认的连接符
      name: true, // 拆分的chunk名,设为true表示根据模块名和CacheGroup的key来自动生成,使用上面连接符连接
      cacheGroups: { // 缓存组配置,上面配置读取完成后进行拆分,如果需要把多个模块拆分到一个文件,就需要缓存,所以命名为缓存组
        vendors: { // 自定义缓存组名
          test: /[\\/]node_modules[\\/]/, // 检查node_modules目录,只要模块在该目录下就使用上面配置拆分到这个组
          priority: -10 // 权重-10,决定了哪个组优先匹配,例如node_modules下有个模块要拆分,同时满足vendors和default组,此时就会分到vendors组,因为-10 > -20
        },
        default: { // 默认缓存组名
          minChunks: 2, // 最少引用两次才会被拆分
          priority: -20, // 权重-20
          reuseExistingChunk: true // 如果主入口中引入了两个模块,其中一个正好也引用了后一个,就会直接复用,无需引用两次
        }
      }
    }
  }

noParse

在引入一些第三方模块时,我们知道其内部肯定不会依赖其他模块,此时再去解析他们的内部依赖关系是非常浪费时间的

  module: {
    noParse: /jquery/,
  },

IgnorePlugin

在引入一些第三方模块时,内部会做i18n国际化处理,可以忽略语言包,然后按需引入 ① 查看源码,分析可得出locale目录就是moment所依赖的语言包目录

function loadLocale(name) {
  var oldLocale = null
  // TODO: Find a better way to register and load all the locales in Node
  if (!locales[name] && typeof module !== 'undefined' && module && module.exports) {
    try {
      oldLocale = globalLocale._abbr
      var aliasedRequire = require
      aliasedRequire('./locale/' + name)
      getSetGlobalLocale(oldLocale)
    } catch (e) {}
  }
  return locales[name]
}
  1. 使用IgnorePlugin插件来忽略掉moment模块的locale目录 参数 1 表示要忽略的资源路径,参数 2 要忽略的资源 context
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
  1. 按需引入语言包
import moment from 'moment'
import 'moment/locale/zh-cn'
moment.locale('zh-CN')
console.log(moment().subtract(6, 'days').calendar())

DllPlugin

在引入一些第三方模块时,vue/react/angular等框架文件一般都是不会修改的,而每次打包都需要去解析,会影响打包速度,做拆分只提高了上线后用户访问速度,并不会提高构建速度,应该使用动态链接库的方式,借助DllPlugin插件实现将这些框架座位一个个的动态链接库,只构建一次,以后每次构建都只生成自己的业务代码,主要将一些不做修改的依赖文件提前打包

vue/react 项目中的库抽取成 Dll

  1. 在 build 目录新建一个文件webpack.vue.jswebpack.react.js 配置入口:将多个要做成dll的库全放进来 配置出口:一定要设置library属性,将打包好的结果全暴露在全局 配置 plugin:设置打包后dll文件名和 manifest 文件所在地
const path = require('path')
const { webpack } = require('webpack')
module.exports = {
  mode: 'produce',
  entry: {
    vue: ['vue/dist/vue.js', 'vue-router'],
    // react: [
    //   'react',
    //   'react-dom'
    // ]
  },
  output: {
    filename: '[name]_dll.js',
    path: path.resolve(__dirname, '../dist'),
    library: '[name]_dll',
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_dll',
      path: path.resolve(__dirname, '../dist/manifest.json'),
    }),
  ],
}
  1. webpack.base.js中进行插件配置,使用DllReferencePlugin指定manifest文件的位置
new webpack.DllReferencePlugin({
  manifest: path.resolve(__dirname, '../dist/manifest.json'),
})
  1. 配置插件自动添加script标签到Html中,安装add-asset-html-webpack-plugin
new AddAssetHtmlWebpackPlugin({
  filepath: path.resolve(__dirname, '../dist/vue_dll.js'),
})

happypack

由于webpacknode环境中运行打包构建,所以是单线程的模式,在打包众多资源时效率会比较低下,早期可以通过Happypack来实现多进程打包。当然,这个问题只出现在低版本的webpack中,现在的webpack性能已经非常强劲了,所以无需使用Happypack也可以实现高性能打包
安装happypack,引入插件修改配置规则,配置插件

const HappyPack = require('happypack')
...
{
  test: /.js$/,
  use: {
      loader: 'happypack/loader'
    },
  include: path.resolve(__dirname, '../src'),
  exclude: /node_modules/
}
...
new HappyPack({
  loaders: [ 'babel-loader' ]
})

浏览器缓存

在做了众多代码分离的优化后,其目的是为了利用浏览器缓存,达到提高访问速度的效果,所以构建项目时做代码分割是必须的,例如将固定的第三方模块抽离,下次修改了业务代码,重新发布上线不重启服务器,用户再次访问服务器就不需要再次加载第三方模块了。如果再次打包上线不重启服务器,客户端会把以前的业务代码和第三方模块同时缓存,再次访问时依旧会访问缓存中的业务代码,所以会导致业务代码也无法更新,需要在output节点的filename中使用placeholder语法,根据代码内容生成文件名的hash,之后每次打包业务代码时,如果有改变,会生成新的hash作为文件名,浏览器就不会使用缓存了,而第三方模块不会重新打包生成新的名字,则会继续使用缓存

output: {
  path: path.join(__dirname,'../dist/'),
  filename: '[name].[contenthash:8].bundle.js',
  publicPath: '/'
},

打包分析 安装webpack-bundle-analyzer -D

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  plugins: [
    new BundleAnalyzerPlugin()
  ],

Prefetching 和 Preloading

Chrome浏览器控制台ctrl+shift+p,查找coverage,可查看覆盖率
在懒加载时使用魔法注释Prefetching,在首页资源加载完毕后空闲时将动态导入的资源加载进来,这样可以提高首屏加载速度,也可以解决懒加载可能会影响用户体验的问题

function getComponent() {
  return import(/* webpackPrefetch: true */ 'jquery').then(({ default: $ }) => {
    return $('<div></div>').html('我是main')
  })
}