webpack性能优化

性能分析

1. 统计基本信息

使用webpack内置的stats

可以统计出构建时间、构建资源清单及资源大小等信息

使用方法:

1. cli

**

css 复制代码
webpack --env production --json > stats.json

2. node API

**

js 复制代码
webpack(config, (err, stats) => {
  console.log(stats);
});

2. 速度分析

使用speek-measure-webpack-plugin

插件功能

  1. 分析出整个构建时间和每个loader和plugin的构建时间
  2. 时间过长的标红,较长的标黄

插件使用:包裹webpack的配置

**

js 复制代码
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();

const webpackConfig = smp({
  // webpack配置
});

3. 体积分析

使用webpack-bundle-analyzer

以可视化形式展示打包依赖模块的体积。

**

js 复制代码
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

构建完成后会启动本地服务,serve 8888端口,浏览器中访问就能看到分析结果。

提升构建速度

使用高版本的webpack和nodejs

高版本的webpack和nodejs构建速度更快

webpack4优化原因:

  • v8带来的优化(for of 代替forEach,Map、Set代替Object、includes代替indexOf)
  • 默认使用更快的md4 hash算法
  • webpack AST可以直接从loader传递给AST,减少解析时间
  • 使用字符串方法代替正则

多进程多实例构建

资源并行解析可选方案

  • HappyPack
    作者已经不维护,建议使用webpack官方提供的"thread-loader"
  • thread-loader

**

json 复制代码
{
  test: /.js$/,
    user: [
      {
        loader: 'thread-loader',
        options: {
          workers: 3
        }
      },
      'babel-loader'
    ]
}
  • parallel-webpack

多进程多实例并行压缩

1. 方法一,使用parallel-uglify-plugin插件

**

js 复制代码
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  plugins: [
    new ParallelUglifyPlugin({
      // ...
    })
  ]
};

2. 方法二,使用uglifyjf-webpack-plugin

目前webpack官方推荐使用terser-webpack-plugin

3. 方法三,使用terser-webpack-plugin,开启parallel参数

**

js 复制代码
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: 4
      })
    ]
  }
};

进一步分包:预编译资源模块

1. 使用html-webpack-externals-plugin

将react、react-dom基础包通过cdn引入,不打入bundle中

使用方法:

**

js 复制代码
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');

plugins: [
  new HtmlWebpackExternalsPlugin({
    externals: [
      {
        module: 'react',
        entry: '//path/to/your/cdn-domain/react.min.js',
        global: 'React'
      },
      ...
    ]
  });
]

效果

**

html 复制代码
<script type="text/javascript" src="//path/to/your/cdn-domain/react.min.js"></script>

2. 预编译资源模块,使用DLLPlugin和DllReferencePlugin

通常来说,我们的代码都可以至少简单区分成业务代码第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。

使用dll时,构建过程分成dll构建过程和主构建过程,所以需要两个构建配置文件,例如叫做webpack.config.jswebpack.dll.config.js

步骤:

  1. 使用DLLPlugin进行分包,对第三方包打包,完成后打包结果保存在项目中,后面就不需要再构建第三方包了。
  2. 每次构建业务项目时候,使用DllReferencePlugin实现对构建好的第三方包dll的解析和处理。

示例:

使用DLLPlugin进行分包

**

js 复制代码
// webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  context: process.cwd(),
  entry: {
    library: [
      'react',
      'react-dom',
      'redux'
    ]
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, './build/library'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: './build/library/[name].json'
    })
  ]
};

使用DllReferencePlugin对manifest.json引用

**

js 复制代码
// webpack.config.js
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./build/library/manifest.json')
    })
  ]
};

引用效果

**

html 复制代码
<script src="/build/library/library.dll.js"></script>

原理

使用DLLPlugin对第三方库打包时候,会生成打包结果和manifest.json文件到指定目录,文件中包含各第三方包的引用关系等信息。

使用DllReferencePlugin插件打包业务代码时候,我们通过配置告诉插件DLLPlugin打包的产物的目录,DllReferencePlugin会分析manifest文件,DLL的包不会参与打包构建过程,并且还生成相关的引用。

3. 使用externals选项

使用externals选项可以排除指定的第三方模块,在构建过程中忽略它们。

使用方法示例:

首先在html中引入第三方模块

**

html 复制代码
<script src="https://example-cdn.com/react.min.js"></script>

然后在externals选项里面配置要排除的模块和引用方式

**

js 复制代码
// webpack.config.js
module.exports = {
  externals: {
    'react': 'React'
  }
};

在项目中引用

**

js 复制代码
import React from 'react';

充分利用缓存提升二次构建速度

缓存思路

  1. babel-loader开启缓存
  2. terser-webpack-plugin开启缓存
  3. 使用cache-loader或者hard-source-webpack-plugin

缩小构建目标

目的:尽可能少构建模块

比如babel-loader不解析node_modules

**

javascript 复制代码
module.exports = {
  rules: {
    test: /.js$/,
    loader: 'happypack-loader',
    exclude: 'node_moudles'
  }
};

减少文件搜索范围

  1. 优化resolve.modules配置(减少模块搜索层级)
  2. 优化resolve.mainFields配置(缩小模块入口搜索范围)
  3. 优化resolve.extensions配置(比如限定.js,其他引用时候补全后缀)
  4. 合理使用alias(缩小模块引用路径搜索范围)

使用oneOf

通常来讲,同一种类型的文件只能由一个loader处理,那么正常来讲的逻辑应该是,比如我是一个css文件,那么我匹配到test为css后缀的loader我就应该立即执行了,但是事实是,虽然匹配到了,但是还是会遍历完整一遍再进行解析,这样来讲效果明显就更低了。

而Oneof语法就是解决这个问题的,使文件一旦匹配上loader之后就立即解析,省去了全盘遍历这个不必要的过程。

如果对于需要多个loader共同解决的文件类型,比如js。那就需要把其中的loader放历Oneof之外,这样才能实现loader也能同时执行到。

提升加载和运行速度

使用Tree Shaking擦除无用的js和css

摇树js

Tree-Shaking原理

Tree-shaking的本质是消除没有用到的代码。主要的效果是,引用了但没有使用的模块,不会被打包到最终的bundle中。

Tree-shaking要求模块是ESM,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。

所谓静态分析就是不运行代码,从语法上对代码进行分析,ES6之前的模块化规范,比如我们可以动态require一个模块,只有执行后才知道是否要引用某个模块,引用的是什么模块(因为require是运行时调用,所以require理论上可以运用在代码的任何地方,而且require支持传入表达式作为参数,只能在运行时才知道引入的是哪个模块),这个就不能通过静态分析去做优化。

webpack默认支持Tree-shaking,如果mode为"production"webpack在构建会做Tree-shaking的操作。

摇树css

摇树css的基本思路是,给定content和css,分析content中用到的选择器,然后分析css文件中没有用到的选择器,将其移除。

摇树css工具有:

  1. PurifyCSS
    使用purgecss-webpack-plugin
  2. uncss

scope-hoisting

Scope Hoisting使用和原理分析

webpack 的 scope hoisting 是什么?

scope hoisting 是 webpack3 的新功能,直译过来就是「作用域提升」。熟悉 JavaScript 都应该知道「函数提升」和「变量提升」,JavaScript 会把函数和变量声明提升到当前作用域的顶部。「作用域提升」也类似于此,webpack 会把引入的 js 文件"提升到"它的引入者顶部。

在之前版本中,webpack打包会把每个模块用闭包封装,通过webpack_require 引用。

这样会存在问题:

  1. ⼤量作用域包裹代码,导致体积增大(模块越多越明显)
  2. 运行代码时创建的函数作⽤域变多(每个模块引用都要创建一个函数作用域),内存开销变大

为了解决这两个问题,webpack启用scope hoisting,将每个模块都提升到引入者顶部,这样模块不会因为依赖链路较深而导致调用栈变深,它们都在同一层。

这样就解决了上面的两个问题:

  1. 代码量明显减少。
  2. 调用栈变浅,减少了创建作用域的内存和计算损耗,提升了运行速度。

使用webpack进行图片压缩

图片压缩实际是使用了基于node库的imagemin或者tinypng API

在webpack中配置image-webpack-loader,这个loader实际是使用了imagemin进行图片压缩

优化polyfill方案

polyfill的方案:

1. babel-polyfill

将babel-polyfill作为一个单独的入口打包

这样做的一个问题是会将所有polyfill代码都打包进去(200k左右),导致代码体积过大

**

js 复制代码
const path = require('path');

module.exports = {
  entry: [
    'babel-polyfill',
    path.resolve(__dirname, './src/index.js')
  ]
};

2. babel-presets的选项中"useBuiltIns"

选项值为"false"时候,不加入polyfill。

选项值为"entry"时候,将所有polyfill打包进项目。

选项值为"usage"时候,按需加载,并且做了优化:将polyfill的工具方法提取成公共资源,而不会每个 polyfill代码都内联相同的工具方法 。

此外babel-presets中还支持根据支持的浏览器来选择polyfill,这通过"target"属性配置。

**

json 复制代码
// .babelrc
{
  "presets": [
    "@babel/preset-env",
    {
      "useBuildIns": "useage"
    }
  ]
}

3. @babel/runtime和@babel/plugin-transform-runtime

@babel/runtime实现polyfill的功能,它分析代码,然后添加相关的polyfill,即实现了polyfill的按需加载。它和上述两种方法的区别是,它在添加polyfill代码时候,不会污染全局变量,而是定义局部方法来实现polyfill。因为这个特点,它更适合用在第三方库中,而上面两种适合用在业务代码项目中

其缺点在于不支持实例方法的polyfill,如arr.includes(1);

由于 @babel/runtime也是使用内联代码实现polyfill,因此可能多个文件中会内联相同的工具方法。@babel/plugin-transform-runtime用来解决这个问题,它提取公共的工具方法,每个文件使用时候引入相关的工具方法,这样减少的代码体积。

4. polyfill-service,使用动态polyfill服务

根据浏览器userAgent选择相应的polyfill,有些浏览器支持的,就不再下发冗余polyfill。

可以使用官方的服务。

或者自建polyfill服务。

可能存在的问题:浏览器ua不准,有些国内浏览器修改ua导致polyfill判断错误,降级方案是下载所有polyfill

**

bash 复制代码
https://cdn.polyfill.io/v2/polyfill.min.js

使用prerender-spa-plugin预渲染

prerender-spa-plugin

prerender-spa-plugin插件启用无头浏览器,加载项目的路由,并渲染出首屏页面(也可以配置其他路由),然后生成静态页面,保存在指定的目录。

我们的静态资源服务器就可以serve预渲染的页面了。

使用这个插件相当于在构建阶段就渲染好了首屏页面,极大地提升了首屏性能。

相关推荐
幼儿园的小霸王3 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue
Amd7945 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You5 小时前
09 —— Webpack搭建开发环境
前端·webpack·node.js
契机再现11 小时前
babel与AST
javascript·webpack·typescript
前端李易安1 天前
Webpack 热更新(HMR)详解:原理与实现
前端·webpack·node.js
loey_ln1 天前
webpack配置和打包性能优化
前端·webpack·性能优化
Amd7941 天前
Nuxt.js 应用中的 webpack:compile 事件钩子
webpack·自定义·编译·nuxt.js·构建·钩子·逻辑
三天不学习1 天前
前端工程化-node/npm/babel/polyfill/webpack 一文速通
前端·webpack·npm
前端青山1 天前
webpack进阶(一)
前端·javascript·webpack·前端框架·node.js
前端与小赵2 天前
什么是Webpack,有什么特点
前端·webpack·node.js