webapck 性能优化

2,448 阅读5分钟

上一章我们介绍了Webpack常用配置和大致优化思路,这一章节我们来看一下具体怎么优化(未完待续,大佬们可以在评论中提点意见)

git地址:github.com/jxs7754/dem…

1. 构建速度优化

  • 高版本的node和Webpack
  • 开启多进程,加快解析、压缩速度
  • 分包,分离基础包
  • 利用缓存来提升二次构建速度
  • 减少文件搜索范围

速度分析:使用speed-measure-webpack-plugin

const SpeedMeasureWebpackPlugin = reqire('speed-measure-webpack-plugin');
const smp = new SeedMeasureWebpackPlugin();
const webpackCofig = smp.wrap({
  plugins:[
    // MyPlugin(),
  ]
})

可以分析整个打包的总耗时,可以查看每个loader和plugins的耗时情况;

1.1 使用高版本的Node和Webapck

  • V8引擎的升级优化
  • webpack4 默认使用更快md4 hash算法
  • webpacks AST 可以直接从 loader 传递给 AST,减少解析时间
  • 使用字符串方法替代正则表达式

1.2 开启多进程

thread-loader

{
  module:{
    rules: [
      {
        test: '/.js$/',
        use: [
          {
            loader: 'thread-loader',
            options:{
              workers: 3,    
            }
          },
          'babel-loader'
        ]
      }
    ]
  }    
}

HappyPack(作者已经不再维护)

const HappyPack = require('happypack');

exports.module = {
  rules: [
    {
      test: /.js$/,
      // 1) replace your original list of loaders with "happypack/loader":
      // loaders: [ 'babel-loader?presets[]=es2015' ],
      use: 'happypack/loader',
      include: [ /* ... */ ],
      exclude: [ /* ... */ ]
    }
  ]
};

exports.plugins = [
  // 2) create the plugin:
  new HappyPack({
    // 3) re-add the loaders you replaced above in #1:
    loaders: [ 'babel-loader?presets[]=es2015' ]
  })
];

多线程压缩

// terser-webpack-plugin
module.exports = {
  optimization: {
    minimizer: {
      new TerserPlugin({
        parallel: 4,  
      })    
    }  
  }    
}
// 下面这俩个插件可以配置多线程
//  parallel-uglify-plugin 
//  uglifyjs-webpack-plugin 

1.3 分包

设置Externals,使用 html-webpack-externals-plugin将基础包(vue vue-router)通过CDN,不打入包中。

new HtmlWebpackExternalsPlugin({
  externals: [
    {
      module: 'react',
      entry: 'https://xxx/react.min.js',
      global: 'React',
    },
    {
      module: 'react-dom',
      entry: 'https://xxx/react-dom.min.js',
      global: 'ReactDOM',
    },
  ],
}),

没有CDN的情况 可以预编译 DllPlugin进行分包,DllReferencePlugin对manifest.json 引用

// 分包
module.exports = {
  mode: 'production',
  entry: {
    vue: ['vue/dist/vue.esm.js', 'vue-router', 'vuex'],
    axios: ['axios', 'qs'],
    // ui: ['element-ui'],
  },
  output: {
    filename: '[name]_[chunkhash:8].dll.js',
    path: path.join(__dirname, 'build'),
    library: '[name]',
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      name: '[name]_[hash]',
      path: path.join(__dirname, 'build/[name].json'),
    }),
  ],
};
// 引用
module.exports = {
  plugins: [
     ...['vue', 'axios'].map((item) => new webpack.DllReferencePlugin({
      context: path.join(__dirname, './build'),
      manifest: require(`./build/${item}.json`),
    })),
  ]    
}

1.4 缓存

缓存是为了二次构建时候,加快构建

babel-loader 开启缓存

{
    loader: 'babel-loader',
    options:{
      cacheDirectory: true    
    }
}    

terser-webpack-plugin 开启缓存

{
  optimization: {
    minimizer: {
      new TerserPlugin({
        // 多线程
        parallel: 4,
        // 缓存
        cache: true,
      })    
    }  
  } 
}

hard-source-webpack-plugin 或者 cache-loader

1.5 减少文件搜素范围

优化loader配置

由于 Loader 对文件的转换操作很耗时,所以需要让尽可能少的文件被 Loader 处理。可以通过 test/include/exclude 三个配置项来命中 Loader 要应用规则的文件。

使用合理的alias

在实战项目中经常会依赖一些庞大的第三方模块,以 Vue 库为例,发布出去的 Vue 库中包含多套代码, vue.runtime.esm.js 中只包含运行时的代码。如果不用template选项可以直接用这个减少打包体积。

module.exports = {
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.runtime.esm.js',    
    }
  }    
}

优化resolve.modules配置

resolve.modules 的默认值是['node_modules'],含义是先去当前目录的node_modules目录下去找我们想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules中找,以此类推。当安装的第三方模块都放在项目根目录的 node_modules 目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。

module.exports = {
  resolve: {
    modules: [path.resolve( __dirname,'node modules')]  
  }    
}

优化resolve.mainFields配置

在安装的第三方模块中都会有一个package.json文件,用于描述这个模块的属性,其中可以存在多个字段描述入口文件,原因是某些模块可以同时用于多个环境中,针对不同的运行环境需要使用不同的代码。 segmentfault.com/a/119000001…

优化resolve.extensions配置

在导入语句没带文件后缀时,Webpack会自动带上后缀去尝试询问文件是否存在。如果这个列表越长,或者正确的后缀越往后,就会造成尝试的次数越多,所以resolve.extensions的配置也会影响到构建的性能在配置resolve.extensions时需要遵守以下几点,以做到尽可能地优化构建性能。

  • 后缀列表尽可能小
  • 频率出现高的文件后缀优先放前面
  • 源码中写导入语句时,尽可能带上后缀
 {
    extensions: ['.js'],
  },

2. 构建体积优化

  • 提取公共代码、分割代码、按需加载、懒加载
  • tree-shaking
  • scope-hoisting
  • 删除无用的css
  • 动态polyfill
  • 代码压缩,开启Gzip压缩

体积分析 webpack-bundle-analyzer

可以分析依赖的第三方模块的大小、业务里面组件的代码大小

const BoundAnalysisPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
  plugins: [
    new BoundAnalysisPlugin(),
  ]
}

2.1 提取公共代码、分割代码、按需加载、懒加载

// 组件按需加载
import {Button} from 'element-ui';

// 模块按需加载
import {cloneDeep} from 'lodash-es';

// Vue 路由懒加载
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      minRemainingSize: 0,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 6,
      maxInitialRequests: 4,
      automaticNameDelimiter: '~',
      automaticNameMaxLength: 30,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }

2.2 tree-shaking

1个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到 bundle 里面去,tree shaking 就是只把用到的方法打入 bundle ,没用到的方法会在 uglify 阶段被擦除掉。 注意事项:

  • mode: production 默认开启 babel设置 modules:false,
  • 必须使用ES6的语法

2.3 删除无用的css

使用 purgecss-webpack-plugin 配合 mini-css-extract-plugin 使用

const config = {
  module:{
      rules: [
        {
          test: '/.scss$/',
          use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: () => [
                // 自动扩展css
                require('autoprefixer')(),
              ],
            },
          },
        }
      ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: getAssetPath(
        `css/[name]_[contenthash:8]'}.css`,
      ),
    }),
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
    }),
  ]
}

2.4 动态polyfill

方案 优点 缺点
babel-polyfill 大而全 体积太大
@babel/plugin-transform-runtime 只polyfill用到的方法和类,体积较小 不能polyfill原型上的方法
polyfill-service 只返回客户需要的polyfill 国内奇葩浏览器

2.5 Scope-Hoisting

ModuleConcatenationPlugin 现在webpack4在mode 不等于none都支持

2.6 图片压缩,代码压缩,还可以开启Gzip压缩

使用 image-webpack-loader进行图片压缩

3. 加载优化

3.1 预加载

使用 @vue/preload-webpack-plugin 实现代码预加载

const config = {
  plugins: [
    new PreloadPlugin({
      rel: 'preload',
      include: 'initial',
      fileBlacklist: [/\.map$/, /hot-update\.js$/],
    }),
    new PreloadPlugin({
      rel: 'prefetch',
      include: 'asyncChunks',
    }),
  ]
}

3.2 使用文件指纹,浏览器缓存

  • Hash:和整个项⽬的构建相关,只要项⽬文件有修改,整个项⽬构建的 hash 值就会更改
  • Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值
  • Contenthash:根据文件内容来定义 hash ,文件内容不不变,则 contenthash 不不变
// js
{
  output: {
    filename: '[name]_[chunkhash:8].js'  
  }    
}
// css
// MiniCssExtractPlugin
{
  plugins:[
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'    
    })
  ]
}

// 图片 
// file-loader 使用hash(这里的hash是根据内容生成的,默认是md5)
{
  module:{
    rules:[
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [{
          loader: 'file-loader’,
          options: {
            name: 'img/[name][hash:8].[ext] ' 
          } 
        }]   
      }
    ]
  }
}

。。。未完待续