webpack性能优化大全

321 阅读4分钟

上一章节我们已经掌握了webpack常见的所有配置

这一节我们来看看如何实现webpack中的优化,我们先来编写最基本的webpack配置,然后依次实现各种优化!

删除无用的Css样式

先来看编写的代码

import './style.css'
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<div>hello</div>,document.getElementById('root'));
body{
    background: red
}
.class1{
    background: red
}

这里的.class1显然是无用的,我们可以搜索src目录下的文件,删除无用的样式

purgecss
可以去除未使用的 css,一般与 glob、glob-all 配合使用
必须和mini-css-extract-plugin配合使用
paths路径是绝对路径
npm i -D purgecss-webpack-plugin mini-css-extract-plugin glob
webpack.config.js

+ const glob = require('glob');
+ const PurgecssPlugin = require('purgecss-webpack-plugin');

{
  test: /\.css/,
  include: path.resolve(__dirname,'src'),
  exclude: /node_modules/,
  use: [{
    loader: MiniCssExtractPlugin.loader
  },'css-loader']
}
module.exports = {
  mode: 'development',
  plugins: [
    new PurgecssPlugin({
      paths:  glob.sync(`${path.join(__dirname, "src")}/**/*`, { nodir: true }) // 不匹配目录,只匹配文件
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename:'[id].css'
    })
  ]
}

注意可能含有副作用,感觉效果也不是特别明显

图片压缩插件

将打包后的图片进行优化

npm install image-webpack-loader --save-dev

在file-loader之前使用压缩图片插件

loader: "image-webpack-loader",
options: {
  mozjpeg: {
    progressive: true,
    quality: 65
  },
  // optipng.enabled: false will disable optipng
  optipng: {
    enabled: false,
  },
  pngquant: {
    quality: [0.90, 0.95],
    speed: 4
  },
  gifsicle: {
    interlaced: false,
  },
  // the webp option will enable WEBP
  webp: {
    quality: 75
  }
}

可以发现图片大小是有了明显的变化

Tree-shaking && Scope-Hoisting

Tree-shaking

顾名思义就是将没用的内容摇晃掉,来看下面代码

main.js

import { minus } from "./calc";
console.log(minus(1,1));

calc.js

import {test} from './test';
export const sum = (a, b) => {
  return a + b + 'sum';
};
export const minus = (a, b) => {
  return a - b + 'minus';
};

test.js

export const test = ()=>{
    console.log('hello')
}
console.log(test());

观察上述代码其实我们主要使用minus方法,test.js代码是有副作用的!

默认mode:production时,会自动tree-shaking,但是打包后'hello'依然会被打印出来,这时候我们需要配置不使用副作用

package.json中配置

"sideEffects":false,

如果这样设置,默认就不会导入css文件啦,因为我们引入css也是通过import './style.css'

重点就来了,tree-shaking主要针对es6模块,我们可以使用require语法导入css,但是这样用起来有点格格不入,所以我们可以配置css文件不是副作用

"sideEffects":[
    "**/*.css"
]

在开发环境下默认tree-shaking不会生效,可以配置标识提示

optimization:{
  usedExports:true 
}

4.2 Scope Hoisting

作用域提升,可以减少代码体积,节约内存

let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;
// 引入d
import d from './d';
console.log(d)

最终打包后的结果会变成 console.log(6)

  • 代码量明显减少
  • 减少多个函数后内存占用也将减少

5.DllPlugin && DllReferencePlugin

每次构建时第三方模块都需要重新构建,这个性能消耗比较大,我们可以先把第三方库打包成动态链接库,以后构建时只需要查找构建好的库就好了,这样可以大大节约构建时间

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h1>hello</h1>,document.getElementById('root'))

DllPlugin

这里我们可以先将reactreact-dom单独进行打包

单独打包创建webpack.dll.js

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
    entry:['react','react-dom'],
    mode:'production',
    output:{
        filename:'react.dll.js',
        path:path.resolve(__dirname,'dll'),
        library:'react'
    },
    plugins:[
        new DllPlugin({
            name:'react',
            path:path.resolve(__dirname,'dll/manifest.json')
        })
    ]
}

执行"webpack --config webpack.dll.js命令,可以看到dll目录下创建了两个文件分别是manifest.json,react.dll.js

关系是这个酱紫的,到时候我们会通过manifest.json找到react.dll.js文件中的模块进行加载

DllReferencePlugin

在我们的项目中可以引用刚才打包好的动态链接库

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
// 构建时会引用动态链接库的内容
new DllReferencePlugin({
  manifest:path.resolve(__dirname,'dll/manifest.json')
}),
// 需要手动引入react.dll.js
new AddAssetHtmlWebpackPlugin(
  { filepath: path.resolve(__dirname,'dll/react.dll.js') }
)

使用DllPlugin可以大幅度提高构建速度

注意在webpack5中,已经移除了此方案

动态加载

实现点击后动态加载文件

let btn = document.createElement('button');
btn.innerHTML = '点击加载视频';
btn.addEventListener('click',()=>{
    import('./video').then(res=>{
        console.log(res.default);
    });
});
document.body.appendChild(btn);

给动态引入的文件增加名字

output:{
  chunkFilename:'[name].min.js'
}
import(/* webpackChunkName: "video" */ './video').then(res=>{
    console.log(res.default);
})

这样打包后的结果最终的文件就是 video.min.js

打包文件分析工具

安装webpack-bundle-analyzer插件

npm install --save-dev webpack-bundle-analyzer

使用插件

const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
mode !== "development" && new BundleAnalyzerPlugin()

默认就会展现当前应用的分析图表

SplitChunks

我们在来看下SplitChunks这个配置,他可以在编译时抽离第三方模块、公共模块

将项目配置成多入口文件

entry:{
  a:'./src/a.js',
  b:'./src/b.js'
}

我们让a,b两个模块同时引用jquery,别忘了去掉之前的externals配置

配置SplitChunks插件

默认配置在此,我一个个描述下含义

splitChunks: {
  chunks: 'async', // 分割异步模块
  minSize: 30000, // 分割的文件最小大小
  maxSize: 0, 
  minChunks: 1, // 引用次数
  maxAsyncRequests: 5, // 最大异步请求数
  maxInitialRequests: 3, // 最大初始化请求数
  automaticNameDelimiter: '~', // 抽离的命名分隔符
  automaticNameMaxLength: 30, // 名字最大长度
  name: true,
  cacheGroups: { // 缓存组
    vendors: { // 先抽离第三方
      test: /[\\/]node_modules[\\/]/,
      priority: -10
    },
    default: { 
      minChunks: 2,
      priority: -20, // 优先级
      reuseExistingChunk: true
    }
  }
}

我们将async改为initial

我们在为每个文件动态导入lodash库,并且改成async

import('lodash')

为每个入口引入c.js,并且改造配置文件

splitChunks: {
  chunks: 'all',
  name: true,
  cacheGroups: {
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      priority: -10
    },
    default: {
      minSize:1, // 不是第三方模块,被引入两次也会被抽离
      minChunks: 2,
      priority: -20,
    }
  }
}

这样再反过来看chunks的参数是不是就了然于胸啦!热更新

模块热替换(HMR - Hot Module Replacement)是 webpack 提供的最有用的功能之一。它允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面

  • 保留在完全重新加载页面时丢失的应用程序的状态
  • 只更新改变的内容,以节省开发时间
  • 调整样式更加快速,几乎等同于就在浏览器调试器中更改样式

启用热更新,默认样式可以支持热更新,如果不支持热更新则采用强制刷新

devServer:{
  hot:true
}
new webpack.NamedModulesPlugin(),

js支持热更新

import sum from './sum';
console.log(sum(1,2));
if(module.hot){ // 如果支持热更新
    module.hot.accept(); // 当入口文件变化后重新执行当前入口文件
}

IgnorePlugin

忽略 importrequire语法

new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)

费时分析

可以计算每一步执行的运行速度

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
  module.exports =smw.wrap({
});

noParse

module.noParse,对类似jq这类依赖库,内部不会引用其他库,我们在打包的时候就没有必要去解析,这样能够增加打包速率

noParse:/jquery/

resolve

  • 减少要处理的文件
  • 缩小查找的范围

缩小查找范围

extensions

  • 指定extensions之后可以不用在req©uire或是import的时候加文件扩展名
  • 查找的时候会依次尝试添加扩展名进行匹配

alias

  • 配置别名可以加快webpack查找模块的速度
  • 每当引入bootstrap模块的时候,它会直接引入bootstrap,而不需要从node_modules文件夹中按模块的查找规则查找
resolve: {
  extensions: [".js",".jsx",".json",".css"],
  alias:{},
  modules:['node_modules']
},

include/exclude

在使用loader时,可以指定哪些文件不通过loader,或者指定哪些文件通过loader

{
  test: /\.js$/,
  use: "babel-loader",
  // include:path.resolve(__dirname,'src'),
  exclude:/node_modules/
},

happypack

多线程打包,我们可以将不同的逻辑交给不同的线程来处理

npm install --save-dev happypack

使用插件

const HappyPack = require('happypack');
rules:[
  {
    test: /\.js$/,
    use: 'happypack/loader?id=jsx'
  },

  {
    test: /\.less$/,
    use: 'happypack/loader?id=styles'
  },
]
new HappyPack({
  id: 'jsx',
  threads: 4,
  loaders: [ 'babel-loader' ]
}),

new HappyPack({
  id: 'styles',
  threads: 2,
  loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
})

preload , prefetch的使用

preload(预先加载)

  • preload通常用于本页面要用到的关键资源,包括关键js、字体、css文件
  • preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度
  • 在资源上添加预先加载的注释,你指明该模块需要立即被使用
  • 一个资源的加载的优先级被分为五个级别,分别是
    • Highest 最高
    • High 高
    • Medium 中等
    • Low 低
    • Lowest 最低
  • 异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是Low

image.png

<link rel="preload" as="script" href="utils.js">

import(
  `./utils.js`
  /* webpackPreload: true */
  /* webpackChunkName: "utils" */
)

prefetch(预先拉取)

  • prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
<link rel="prefetch" href="utils.js" as="script">

button.addEventListener('click', () => {
  import(
    `./utils.js`
    /* webpackPrefetch: true */
    /* webpackChunkName: "utils" */
  ).then(result => {
    result.default.log('hello');
  })
});

preload vs prefetch

  • preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源
  • 而 prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源
  • 所以建议:对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch

利用缓存

  • webpack中利用缓存一般有以下几种思路:
    • babel-loader开启缓存
    • 使用cache-loader
    • 使用hard-source-webpack-plugin

babel-loader

  • Babel在转义js文件过程中消耗性能较高,将babel-loader执行的结果缓存起来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: [{
      loader: "babel-loader",
      options: {
        cacheDirectory: true
      }
    }]
  },

cache-loader

  • 在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里
  • 存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader

cnpm i cache-loader -D

const loaders = ['babel-loader'];
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'cache-loader',
          ...loaders
        ],
        include: path.resolve('src')
      }
    ]
  }
}

日志美化

安装

cnpm i friendly-errors-webpack-plugin node-notifier -D

2.1.2 webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const notifier = require('node-notifier');
const ICON = path.join(__dirname, 'icon.jpg');
module.exports = {
  mode: "development",
  devtool: 'source-map',
  context: process.cwd(),
  entry: {
    main: "./src/index.js",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "main.js"
  },
  plugins:[
    new HtmlWebpackPlugin(),
    new FriendlyErrorsWebpackPlugin({
      onErrors: (severity, errors) => {
        const error = errors[0];
        notifier.notify({
          title: "Webpack编译失败",
          message: severity + ': ' + error.name,
          subtitle: error.file || '',
          icon: ICON
        });
      }
    })
  ]
};

速度分析

安装

cnpm i speed-measure-webpack5-plugin -D

webpack.config.js

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack5-plugin');
const smw = new SpeedMeasureWebpackPlugin();
module.exports = smw.wrap({
  mode: "development",
  devtool: 'source-map',
  ...
});

文件体积监控

  • webpack-bundle-analyzer是一个webpack的插件,需要配合webpack和webpack-cli一起使用。这个插件的功能是生成代码分析报告,帮助提升代码质量和网站性能
  • 它可以直观分析打包出的文件包含哪些,大小占比如何,模块包含关系,依赖项,文件是否重复,压缩后大小如何,针对这些,我们可以进行文件分割等操作。

安装

cnpm i webpack-bundle-analyzer -D

编译启动

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

单独启动

webpack.config.js
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
module.exports={
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
      generateStatsFile: true, // 是否生成stats.json文件
    }),
  ]
}


"scripts": {
  "build": "webpack",
  "start": "webpack serve",
  "dev":"webpack  --progress",
  "analyzer": "webpack-bundle-analyzer --port 8888 ./dist/stats.json"
}