乐趣区

关于javascript:爆肝总结万字长文笔记webpack5打包资源优化

webpack如何打包资源优化你有理解吗?或者一个常常被问的面试题,首屏加载如何优化,其实无非就是从 http 申请、文件资源 图片加载 路由懒加载 预申请 缓存 这些方向来优化,通常在应用脚手架中,成熟的脚手架曾经给你做了最大的优化,比方压缩资源,代码的 tree shaking 等。

本文是笔者依据以往教训以及浏览官网文档总结的一篇对于 webpack 打包 方面的长文笔记,心愿在我的项目中有所帮忙。

注释开始 …

在浏览之前,本文将从以下几个点去探讨 webpack 的打包优化

1、webpack如何做treeShaking

2、webpack的 gizp 压缩

3、css如何做treeShaking,

4、入口依赖文件 拆包

5、图片资源 加载优化

treeShaking

在官网中有提到 treeShaking, 从名字上中文解释就是摇树,就是利用 esModule 的个性,删除上下文未援用的代码。因为 webpack 能够依据 esModule 做动态剖析,自身来说它是打包编译前输入,所以 webpack 在编译 esModule 的代码时就能够做上下文未援用的删除操作。

那么如何做treeshaking? 咱们来剖析下

疾速初始化一个 webpack 我的项目

在之前咱们都是手动配置搭建 webpack 我的项目,webpack官网提供了 cli 疾速构建根本模版,无需像之前一样手动配置 entrypluginsloader

首先装置npm i webpack webpack-cli,命令行执行 `

npx webpack init

一系列初始化操作后,就生成以下代码了

默认的webpack.config.js

// Generated using webpack-cli https://github.com/webpack/webpack-cli

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const WorkboxWebpackPlugin = require("workbox-webpack-plugin");
const isProduction = process.env.NODE_ENV == "production";
const stylesHandler = MiniCssExtractPlugin.loader;
const config = {
  entry: "./src/index.js",
  output: {path: path.resolve(__dirname, "dist"),
  },
  devServer: {
    open: true,
    host: "localhost",
  },
  plugins: [
    new HtmlWebpackPlugin({template: "index.html",}),

    new MiniCssExtractPlugin(),

    // Add your plugins here
    // Learn more about plugins from https://webpack.js.org/configuration/plugins/
  ],
  module: {
    rules: [
      {test: /\.(js|jsx)$/i,
        loader: "babel-loader",
      },
      {
        test: /\.less$/i,
        use: [stylesHandler, "css-loader", "postcss-loader", "less-loader"],
      },
      {
        test: /\.css$/i,
        use: [stylesHandler, "css-loader", "postcss-loader"],
      },
      {test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
        type: "asset",
      },

      // Add your rules for custom modules here
      // Learn more about loaders from https://webpack.js.org/loaders/
    ],
  },
};
module.exports = () => {if (isProduction) {
    config.mode = "production";

    config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
  } else {config.mode = "development";}
  return config;
};

运行命令npm run serve

当初批改一下 index.js, 并在src 中减少 utils 目录

// utils/index.js
export function add(a, b) {return a + b}
export function square(x) {return x * x;}

index.js

import {add} from './utils'
console.log("Hello World!");
console.log(add(1, 2))

index.js 中我只引入了 add,相当于square 这个函数在上下文中并未援用。

usedExports

不过我还须要改下webpack.config.js

...
module.exports = () => {if (isProduction) {
    config.mode = "production";
    config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
  } else {
    config.mode = "development";
    config.devtool = 'source-map'
    config.optimization = {usedExports: true}
  }
  return config;
};

留神我只减少了 devtool:source-mapoptimization.usedExports = true

咱们看下package.json

 "scripts": {
    "test": "echo \"Error: no test specified\"&& exit 1",
    "build": "webpack --mode=production --node-env=production",
    "build:dev": "webpack --mode=development",
    "build:prod": "webpack --mode=production --node-env=production",
    "watch": "webpack --watch",
    "serve": "webpack serve"
  },

默认初始化曾经给们预设了多个不同的打包环境,因而我只须要运行上面命令就能够抉择开发环境了

npm run build:dev

此时咱们看到打包后的代码未引入的 square 有一行正文

/* unused harmony export square */
function add(a, b) {return a + b;}
function square(x) {return x * x;}

square上下文未援用,尽管给了标记,然而未真正革除。

光应用 usedExports:true 还不行,usedExports 依赖于 terser 去检测语句中的副作用 , 因而须要借助terser 插件一起应用,官网 webpack5 提供了 TerserWebpackPlugin 这样一个插件

webpack.config.js 中引入

...
const TerserPlugin = require("terser-webpack-plugin");
...
module.exports = () => {if (isProduction) {
    config.mode = "production";
    config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
  } else {
    config.mode = "development";
    config.devtool = 'source-map'
    config.optimization = {
      usedExports: true, // 设置为 true 通知 webpack 会做 treeshaking
      minimize: true, // 开启 terser
      minimizer: [new TerserPlugin({extractComments: false,  // 是否将正文剥离到独自文件,默认是 true})]
    }
  }
  return config;
};

你会发现,那个 square 函数就没有了

如果我将 usedExports.usedExports = false, 你会发现square 没有被删除。

官网解释,当咱们设置 optimization.usedExports 必须为true, 当咱们设置usedExports:true,且必须开起minimize: true,这样才会把上下文未应用的代码给革除掉,如果minimize: false, 那么压缩插件将会生效。

当咱们设置usedExports: true

此时生成打包的代码会有一个这样的魔法正文,square未应用

/* unused harmony export square */
function add(a, b) {return a + b;}
function square(x) {return x * x;}

当咱们设置 minimize: true 时,webpack5会默认开启 terser 压缩,而后发现有这样的 unused harmony export square 就会删掉对应未引入的代码。

sideEffects

这个是 usedExports 摇树的另一种计划,usedExports是查看上下文有没有援用,如果没有援用,就会注入 魔法正文 ,通过terser 压缩进行去除未引入的代码

slideEffects 是对 没有副作用 的代码进行去除

首先什么是 副作用 ,这是一个不太好了解的词,在react 中常常有听到

其实 副作用 就是一个纯函数中存在可变依赖的因变量,因为某个因变量会造成纯函数产生不可控的后果

举个例子

没有副作用的函数,输入输出很明确

function watchEnv(env) {return env === 'prd' ? 'product': 'development'}
watchEnv('prd')

有副作用, 函数体内有不确定性因素

export function watchEnv(env) {const num = Math.ceil(Math.random() * 10);
  if (num < 5) {env = 'development'}
  return env === 'production' ? '生产环境' : '测试开发环境'
}

咱们在 index.js 中引入watch.js

import {add} from './utils'
import './utils/watch.js';
console.log("Hello World!");

console.log(add(1, 2))

而后运行 npm run build:dev, 打包后的文件有watch 的引入

index.js 中引入 watch.js 并没有什么应用, 然而咱们依然打包了进去

为了去除这引入但未被应用的代码,因而你须要在 optimization.sideEffects: true,并且要在package.json 中设置 sideEffects: false,在optimization.sideEffects 设置 true, 告知 webpack 依据 package.json 中的 sideEffects 标记的副作用或者规定,从而告知 webpack 跳过一些引入但未被应用的模块代码。具体参考 optimization.sideEffects

module.exports = () => {if (isProduction) {
    config.mode = "production";

    config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
  } else {
    config.mode = "development";
    config.devtool = 'source-map',
      config.optimization = {
        sideEffects: true, // 开启 sideEffects
        usedExports: true,
        minimize: true, // 开启 terser
        minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})]
      }
  }
  return config;
};
{
  "name": "my-webpack-project",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "sideEffects": false,
  ...
}

此时你运行命令npm run build:dev,查看打包文件

咱们就会发现,引入的 watch.js 就没有了

在官网中有这么一段话 应用 mode 为 "production" 的配置项以启用更多优化项,包含压缩代码与 tree shaking。

因而在 webpack5 中只有你设置 mode:production 那些代码压缩、tree shaking统统默认给你做了做了最大的优化,你就无需操心代码是否有被压缩,或者 tree shaking 了。

对于是否被 tree shaking 还补充几点

1、肯定是 esModule 形式,也就是 export xxx 或者 import xx from 'xxx' 的形式

2、cjs形式不能被tree shaking

3、线上打包生产环境 mode:production 主动开启多项优化,能够参考生产环境的构建 production

gizp 压缩

首先是是在 devServer 下提供了一个开发环境的compress:true

{
  devServer: {
    open: true,
    host: "localhost",
    compress: true // 启用 zip 压缩
  }
}
  • CompressionWebpackPlugin 插件 gizp 压缩

须要装置对应插件

npm i compression-webpack-plugin --save-dev

webpack.config.js中引入插件

// Generated using webpack-cli https://github.com/webpack/webpack-cli
...
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const config = {
  ...
  plugins: [
    new HtmlWebpackPlugin({template: "index.html",}),
    new MiniCssExtractPlugin(),
    new CompressionWebpackPlugin(),
    // Add your plugins here
    // Learn more about plugins from https://webpack.js.org/configuration/plugins/
  ],
  ...
};

当你运行命令后,你就会发现打包后的文件有 gzip 的文件了

然而咱们发现 html 以及 map.js.map 文件也被 gizp 压缩了,这是没有必要的

官网提供了一个 exclude, 能够排除某些文件不被gizp 压缩

{
   plugins: [
    new HtmlWebpackPlugin({template: "index.html",}),

    new MiniCssExtractPlugin(),
    new CompressionWebpackPlugin({exclude: /.(html|map)$/i // 排除 html,map 文件
    })
    // Add your plugins here
    // Learn more about plugins from https://webpack.js.org/configuration/plugins/
  ],
}

比照开启 gizp 压缩与未压缩, 加载工夫很显著有晋升

css tree shaking

次要删除未应用的款式,如果款式未应用,就删除掉。

当初批改下 index.js
我在 body 中插入一个class

import {add} from './utils'
import './utils/watch';
import './css/index.css'
console.log("Hello World!");

console.log(add(1, 2))
// /*#__PURE__*/ watchEnv(process.env.NODE_ENV)

const bodyDom = document.getElementsByTagName('body')[0]
const divDom = document.createElement('div');
divDom.setAttribute('class', 'wrap-box');
bodyDom.appendChild(divDom);

对应的 css 如下

.wrap-box {
  width: 100px;
  height: 100px;
  background-color: red;
}

执行npm run serve

然而咱们发现,款式竟然没了

于是苦思瞑想,不得其解, 于是一顿排查,当咱们把 sideEffects: false 时,神奇的是,款式没有被删掉

原来是 sideEffects:true 把引入的 css 当成没有副作用的代码给删除了,此时,你须要通知 webpack 不要删除我的这有用的代码, 不要误删了,因为 import 'xxx.css' 如果设置了 sideEffects: true,此时引入的css 会被当成无副作用的代码,就给删除了。

// package.json
{
  "sideEffects": ["**/*.css"],
}

当你设置完后,页面就能够失常显示 css 了

官网也提供了另外一种计划,你能够在 module.rules 中设置

{
  module: {
    rules: [
         {
        test: /\.css$/i,
        sideEffects: true,
        use: [stylesHandler, "css-loader", "postcss-loader"],
      },
    ]
  }
}

以上与在 package.json 设置一样的成果,都是让 webpack 不要误删了无副作用的 css 的代码

然而当初有这样的 css 代码

.wrap-box {
  width: 100px;
  height: 100px;
  background-color: red;
}
.title {color: green;}

title页面没有被援用,然而也被打包进去了

此时须要一个插件来帮忙咱们来实现 css 的摇树 purgecss-webpack-plugin

const path = require("path");
...
const glob = require('glob');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const PATH = {src: path.resolve(__dirname, 'src')
}
const config = {
  ...
  plugins: [
    ...
    new PurgeCSSPlugin({paths: glob.sync(`${PATH.src}/**/*`, {nodir: true}),
    })

    // Add your plugins here
    // Learn more about plugins from https://webpack.js.org/configuration/plugins/
  ],
  ...
};

未援用的 css 就曾经被删除了

分包

次要是缩小入口依赖文件包的体积,如果不进行拆包,那么咱们依据 entry 的文件打包就很大。那么也会影响首页加载的性能。

官网提供了两种计划:

  • entry 分多个文件,举个栗子

引入loadsh

// index.js
import {add} from './utils';
import _ from 'loadsh';
import './utils/watch';
import './css/index.css';
console.log("Hello World!");

console.log(add(1, 2))
// /*#__PURE__*/ watchEnv(process.env.NODE_ENV)

const bodyDom = document.getElementsByTagName('body')[0]
const divDom = document.createElement('div');
divDom.setAttribute('class', 'wrap-box');
divDom.innerText = 'wrap-box';
bodyDom.appendChild(divDom);

console.log(_.last(['Maic', 'Web 技术学苑']));

main.js中将 loadsh 打包进去了, 体积也十分之大72kb

咱们当初利用 entry 进行分包

const config = {
 entry: {main: { import: ['./src/index'], dependOn: 'loadsh-vendors' },
    'loadsh-vendors': ['loadsh']
  },
}

此时咱们再次运行 npm run build:dev
此时 main.js 的大小 1kb,然而loadsh 曾经被分离出来了

生成的 loadsh-vendors.js 会被独自引入

能够看下打包后的index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Webpack App</title>
    <script defer src="main.js"></script>
    <script defer src="loadsh-vendors.js"></script>
    <link href="main.css" rel="stylesheet" />
  </head>
  <body>
    <h1>Hello world!</h1>
    <h2>Tip: Check your console</h2>
  </body>

  <script>
    if ('serviceWorker' in navigator) {window.addEventListener('load', () => {
        navigator.serviceWorker
          .register('service-worker.js')
          .then((registration) => {console.log('Service Worker registered:', registration);
          })
          .catch((registrationError) => {console.error('Service Worker registration failed:', registrationError);
          });
      });
    }
  </script>
</html>
  • splitChunks
    次要是在 optimzation.splitChunks 对于动静导入模块,在 webpack4+ 就默认采取分块策略

    const config = {
    // entry: {//   main: { import: ['./src/index'], dependOn: 'loadsh-vendors' },
    //   'loadsh-vendors': ['loadsh']
    // },
    entry: './src/index.js',
    ...
    }
    module.exports = () => {if (isProduction) {
      config.mode = "production";
      config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
    } else {
      config.mode = "development";
      config.devtool = 'source-map',
        config.optimization = {
          splitChunks: {chunks: 'all' // 反对异步和非异步共享 chunk},
          sideEffects: true,
          usedExports: true,
          minimize: true, // 开启 terser
          minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})]
        }
    }
    return config;
    };
    

    optimization.splitChunks.chunks:'all', 此时能够把loash 分包进去了

对于 optimization.splitChunks 的设置十分之多,有对缓存的设置,有对 chunk 大小的限度,最罕用的还是设置chunks:all,倡议 SplitChunksPlugin 多读几遍, 肯定会找到不少播种。

  • runtimeChunk
    次要缩小依赖入口文件打包体积,当咱们设置 optimization.runtimeChunk 时,运行时依赖的代码会独立打包成一个runtime.xxx.js

    ...
    config.optimization = {
          runtimeChunk: true, // 缩小入口文件打包的体积,运行时代码会独立抽离成一个 runtime 的文件
          splitChunks: {
            minChunks: 1, // 默认是 1,能够不设置
            chunks: 'all', // 反对异步和非异步共享 chunk
          },
          sideEffects: true,
          usedExports: true,
          minimize: true, // 开启 terser
          minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})]
        }

    main.js有一部分代码移除到一个独立的 runtime.js

  • Externals 内部扩大
    第三种计划就是,webpack提供了一个内部扩大,将输入的 bundle.js 排除第三方的依赖,参考 Externals
const config = {
  // entry: {//   main: { import: ['./src/index'], dependOn: 'loadsh-vendors' },
  //   'loadsh-vendors': ['loadsh']
  // },
  entry: './src/index.js',
  ...,
  externals: /^(loadsh)$/i,
  /* or 
  externals: {loadsh: '_'}
  */
};
 module.exports = () => {if (isProduction) {
    config.mode = "production";
    config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
  } else {
    config.mode = "development";
    config.devtool = 'source-map',
      config.optimization = {
        runtimeChunk: true, // 缩小入口文件打包的体积,运行时代码会独立抽离成一个 runtime 的文件
        // splitChunks: {
        //   minChunks: 1,
        //   chunks: 'all', // 反对异步和非异步共享 chunk
        // },
        sideEffects: true,
        usedExports: true,
        minimize: true, // 开启 terser
        minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})]
      }
  }
  return config;
};

然而此时 loash 曾经被咱们移除了,咱们还需在 HtmlWebpackPlugin 中退出引入的 cdn 地址

...
plugins: [
 new HtmlWebpackPlugin({
      template: "index.html",
      inject: 'body', // 插入到 body 中
      cdn: {
        basePath: 'https://cdn.bootcdn.net/ajax/libs',
        js: ['/lodash.js/4.17.21/lodash.min.js']
      }
    }),
]

批改模版, 因为模版内容是 ejs,所以咱们循环取出 js 数组中的数据

 <!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Webpack App</title>
  </head>
  <body>
    <h1>Hello world!</h1>
    <h2>Tip: Check your console</h2>
    <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.basePath %><%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
    <% } %>
  </body>

  <script>
    if ('serviceWorker' in navigator) {window.addEventListener('load', () => {
        navigator.serviceWorker
          .register('service-worker.js')
          .then((registration) => {console.log('Service Worker registered:', registration);
          })
          .catch((registrationError) => {console.error('Service Worker registration failed:', registrationError);
          });
      });
    }
  </script>
</html>

此时你运行命令npm run build:dev, 而后关上 html 页面

然而咱们发现当咱们运行 npm run serve 启动本地服务,此时页面还是会引入 loadsh,在开发环境,其实并不需要引入,自身生成的bundle.js 就是在内存中加载的,很显然不是咱们须要的

此时我须要做几件事

1、开发环境我不容许引入externals

2、模版 html 中须要依据环境判断是否须要插入cdn

  const isProduction = process.env.NODE_ENV == "production";

const stylesHandler = MiniCssExtractPlugin.loader;

const PATH = {src: path.resolve(__dirname, 'src')
}
const config = {
  // entry: {//   main: { import: ['./src/index'], dependOn: 'loadsh-vendors' },
  //   'loadsh-vendors': ['loadsh']
  // },
  entry: './src/index.js',
  output: {path: path.resolve(__dirname, "dist"),
  },
  devServer: {
    open: true,
    host: "localhost",
    compress: true
  },
  plugins: [
    new HtmlWebpackPlugin({
      env: process.env.NODE_ENV, // 传入模版中的环境
      template: "index.html",
      inject: 'body', // 插入到 body 中
      cdn: {
        basePath: 'https://cdn.bootcdn.net/ajax/libs',
        js: ['/lodash.js/4.17.21/lodash.min.js']
      }
    }),

    new MiniCssExtractPlugin(),
    new CompressionWebpackPlugin({exclude: /.(html|map)$/i // 排除 html,map 文件不做 gizp 压缩
    }),
    new PurgeCSSPlugin({paths: glob.sync(`${PATH.src}/**/*`, {nodir: true}),
    })

    // Add your plugins here
    // Learn more about plugins from https://webpack.js.org/configuration/plugins/
  ],
  ...
  // externals: /^(loadsh)$/i,
  externals: isProduction ? {loadsh: '_'} : {}};

依据传入模版的 env 判断是否须要插入 cdn

  ...
   <% if (htmlWebpackPlugin.options.env === 'production') { %> 
     <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.basePath %><%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
    <% } %>
<% } %>

图片资源压缩

次要是有抉择的压缩图片资源,咱们能够看下module.rules.parser

  • module.rules.parser.dataUrlCondition
    对应的资源文件能够限度图片的输入, 比方动态资源模块类型

      module: {
       rules: [
        {test: /\.(png|svg|jpg|jpeg|gif)$/i,
          type: 'asset/resource',
         parser: {
           dataUrlCondition: {maxSize: 4 * 1024 // 小于 4kb 将会 base64 输入}
         }
        },
       ],
     },

    官网提供了一个 ImageMinimizerWebpackPlugin
    咱们须要装置

    npm i image-minimizer-webpack-plugin imagemin --save-dev

    webpack.config.js 中引入 image-minimizer-webpack-plugin, 并且在plugins 中引入这个插件, 留神 webpack5 官网那份文档很旧,参考 npm 上 npm-image-minimizer-webpack-plugin

依照官网的,就间接报错一些配置参数不存在,我预计文档没及时更新

...
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const config = {
 plugins: [
   ...
    new ImageMinimizerPlugin({
      minimizer: {
        // Implementation
        implementation: ImageMinimizerPlugin.squooshMinify,
      },
    })

    // Add your plugins here
    // Learn more about plugins from https://webpack.js.org/configuration/plugins/
  ],
}

未压缩前

压缩后

应用压缩后,图片无损压缩体积大小压缩大小放大一半,并且网络加载图片工夫从 18.87ms 缩小到4.81ms, 工夫加载上靠近 5 倍的差距,因而能够用这个插件来优化图片加载。

这个插件能够将图片转成 webp 格局,具体参考官网文档成果测试一下

总结

1、webpack如何做treeShaking,次要是两种

  • optimization 中设置 usedExports:true,然而要配合terser 压缩插件才会失效
  • optimization 中设置 sideEffects: true, 在package.json 中设置 sideEffects:false 去除无副作用的代码,然而留神 css 引入会当成无副作用的代码,此时须要在 rules 的 css 规定中标记sideEffects: true, 这样就不会删除 css 了

2、webpack的 gizp 压缩
次要是利用 CompressionWebpackPlugin 官网提供的这个插件

3、css如何做 treeShaking,
次要是利用 PurgeCSSPlugin 这个插件,会将没有援用 css 删除

4、入口依赖文件拆包

  • 第一种是在入口文件 entry 中分包解决,将依赖的第三方库独立打包成一个专用的bundle.js, 入口文件不会把第三方包打包到外面去
  • 第二种利用 optimization.splitChunks 设置 chunks:'all' 将同步或者异步的 esModule 形式的代码进行分包解决,会独自打成一个专用的 js
  • 利用外置扩大 externals 将第三方包拆散进来,此时第三方包不会打包到入口文件中去,不过留神要在 ejs 模版中进行独自引入

5、图片资源 加载优化

  • 次要是利用动态资源模块对文件体积小的能够进行 base64
  • 利用社区插件 image-minimizer-webpack-plugin 做图片压缩解决

6、本文示例 code-example

欢送关注公众号:Web 技术学苑
好好学习,天天向上!

退出移动版