关于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技术学苑
好好学习,天天向上!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理