阅读 3302
webpack打包优化方向指南(理论篇)

webpack打包优化方向指南(理论篇)

对一位合格的前端开发工程师来说,完成业务功能的需求开发只是基本的要求,能够及时准确地发现系统中存在的性能瓶颈,并且给出合适的解决方案,这才是区分初,中级前端工程师和高级前端工程师的重要依据。

对于什么是webpack,这里我就不多做解释,要是你还不知道,那本片文章就不适合现在的你阅读。

webpack链接,看完之后记得过来继续阅读

webpack5 基本配置

我们先来看一份基本的配置,基于webpack5的webpack.config.js文件。

/*
* webpack.config.js
*/
const { resolve } = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // 模式
  mode: "development",
  // 入口
  entry: "./src/index.js",
  // 出口
  output: {
    filename: "built.js",
    path: resolve(__dirname, "build"),
    // 自定义输出 静态资源文件名(图片)
    assetModuleFilename: "assets/[hash][ext]",
  },
  // 模块
  module: {
    rules: [
      // loader的配置
      {
        test: /\.css$/,
        // 使用loader对文件进行处理
        /**
         * use数组中的执行顺序是 从右到左  从下到上 依次执行
         * style-loader  创建style标签,将样式文件引入到header中
         * css-loader 将css模块变成commonjs模块加载到js中, 文件内容是样式字符串
         */
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          "css-loader",
          // 将less文件编译成css文件
          // 需要下载 less-loader和less
          "less-loader",
        ],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        // webpack 5 内置了资源类型,已经废弃了之前的 url-loader 和 file-loader
        type: "asset/resource",
      },
      {
        test: /\.html$/,
        // 处理html中的图片文件 引入img文件进而让url-loader处理
        loader: "html-loader",
      },
      {
        // 处理其他资源
        exclude: /\.(html|js|css|less|png|svg|jpg|jpeg|gif)$/i,
        type: "asset/resource",
      },
    ],
  },
  // 插件
  plugins: [
    // HtmlWebpackPlugin
    // 默认会创建一个空的html文件,会自动引入打包完成的所有资源。
    // 需要有结构的html文件
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
  ],
  /**
   * 下载 yarn add webpack-dev-server --dev
   * 运行  npx webpack serve
   */
  devServer: {
    // 项目构建后的路径
    contentBase: resolve(__dirname, "build"),
    // 自动打开浏览器
    open: true,
    // 端口号
    port: 5555,
    // 开启gzip压缩
    compress: true,
  },
};
复制代码

上面的代码是我们这篇文章的最基础的代码,后边的打包优化就是基于这个文件来进行配置的。

HMR 热更新

模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。

优点:

  1. 保留在完全重新加载页面期间丢失的应用程序状态。
  2. 只更新变更内容,以节省宝贵的开发时间。
  3. 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

问题图片

HRM问题.gif

我们可以从上图看到,我们在改变样式的时候,我们的js文件重新执行了一遍,这样对于我们的项目来说是一个问题,我们的优化方向是改变那个文件,只有该文件重新加载。

代码

根据上边的问题,我们只需要改变devServer处的代码。

/*
* webpack.config.js
*/
...
module.exports = {
...
  devServer: {
    // 项目构建后的路径
    contentBase: resolve(__dirname, "build"),
    // 自动打开浏览器
    open: true,
    // 端口号
    port: 5555,
    // 开启gzip压缩
    compress: true,
    // 新增---> 开启热更新
    // 模块热替换功能会在程序运行过程中,替换,添加或删除模块,而无需重新加载整个页面。
    hot: true,
  },
};
复制代码

效果图片

HRM解决.gif

devtool 源码调试方式

选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

  1. 对于开发环境的源码调试

    eval-source-map - 初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。

  2. 对于生产环境的源码调试

    (none)(省略 devtool 选项) - 不生成 source map。这是一个不错的选择。

Rule.oneOf 匹配规则

loader的匹配规则。

/*
* webpack.config.js
*/
...
module.exports = {
...
// 模块
  module: {
    rules: [
      // loader的配置
      {
        oneOf: [
          // 以下的loader只会执行匹配的文件一次。
          {
            test: /\.css$/,
            // 使用loader对文件进行处理
            /**
             * use数组中的执行顺序是 从右到左  从下到上 依次执行
             * style-loader  创建style标签,将样式文件引入到header中
             * css-loader 将css模块变成commonjs模块加载到js中, 文件内容是样式字符串
             */
            use: ["style-loader", "css-loader"],
          },
          {
            test: /\.less$/,
            use: [
              "style-loader",
              "css-loader",
              // 将less文件编译成css文件
              // 需要下载 less-loader和less
              "less-loader",
            ],
          },
          {
            test: /\.(png|svg|jpg|jpeg|gif)$/i,
            // webpack 5 内置了资源类型,已经废弃了之前的 url-loader 和 file-loader
            type: "asset/resource",
          },
          {
            test: /\.html$/,
            // 处理html中的图片文件 引入img文件进而让url-loader处理
            loader: "html-loader",
          },
          {
            // 处理其他资源
            exclude: /\.(html|js|css|less|png|svg|jpg|jpeg|gif)$/i,
            type: "asset/resource",
          },
        ],
      },
    ],
  },
...
}
复制代码

如此写法,在打包时loader的匹配将提高速度。

文件缓存

在生产环境时,我们可以将我们打包的css和js等资源缓存到浏览器中,以提高我们第二次进入的页面是的速度。所以我们得对webpack.config.js进行配置,对js文件和css文件和图片文件进行配置缓存

首先我们得下载如下loader:

yarn add babel-loader @babel/core @babel/preset-env mini-css-extract-plugin --dev

优化步骤

  1. 将css文件从打包文件中提取为单独的文件。
  2. 对js文件使用babel缓存。

提取css并文件资源缓存

/*
* webpack.config.js
*/
...
// 新增插件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
...
// 模式
mode: "production",
output: {
    filename: "built.[contenthash:10].js",
    ...
  },
...
  // 模块
  module: {
    rules: [
      // loader的配置
      {
        oneOf: [
          // 以下的loader只会执行匹配的文件一次。
          {
            test: /\.css$/,
            // 将 style-loader 替换
            use: [MiniCssExtractPlugin.loader, "css-loader"],
          },
          {
            test: /\.less$/,
            use: [
            // 将 style-loader 替换
              MiniCssExtractPlugin.loader,
              "css-loader",
              "less-loader",
            ],
          },
         ...
         // 新增js的loader
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: "babel-loader",
              options: {
                presets: ["@babel/preset-env"],
                // 开启babel缓存
                // 第二次构建时,会读取之前的缓存
                // 用来缓存 loader的执行结果。之后的webpack 构建,将会尝试读取缓存,
                // 来避免在每次执行时,可能产生的、高性能消耗的 Babel 
                // 重新编译过程(recompilation process)。
                cacheDirectory: true,
              },
            },
          },
        ],
      },
    ],
  },
  // 插件
  plugins: [
    ...
    //新增插件
    new MiniCssExtractPlugin({
      filename: "css/built.[contenthash:10].css",
    }),
  ],
};
复制代码
  1. hash: 每次wepack构建时会生成一个唯一的hash值。

    • 问题: 因为js和css同时使用一个hash值。
    • 如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
  2. chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样。

    • 问题: js和css的hash值还是一样的
    • 因为css是在js中被引入的,所以同属于一个chunk
  3. contenthash: 根据文件的内容生成hash值。

    • 不同文件hash值一定不一样,让代码上线运行缓存更好使用。

新增jsloader并设置缓存

/*
* webpack.config.js
*/
...
module.exports = {
...
  // 模块
  module: {
    rules: [
      // loader的配置
      {
        oneOf: [
         ...
         // 新增js的loader
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: "babel-loader",
              options: {
                presets: ["@babel/preset-env"],
                // 开启babel缓存
                // 第二次构建时,会读取之前的缓存
                // 用来缓存 loader的执行结果。之后的webpack 构建,将会尝试读取缓存,
                // 来避免在每次执行时,可能产生的、高性能消耗的 Babel 
                // 重新编译过程(recompilation process)。
                cacheDirectory: true,
              },
            },
          },
        ],
      },
    ],
  },
...
};
复制代码

接下来我们书写测试代码。新建server.js文件,创建新的服务器进行测试。下载express框架。

yarn add express --dev

/*
* server.js
*/
const express = require("express");

const server = express();

// 缓存一个小时
server.use(express.static("build", { maxAge: 1000 * 3600 }));

server.listen(5555, () => {
  console.log("服务器启动成功!", "http://localhost:5555/");
});
复制代码

检查缓存步骤

  1. 进行webpack打包 webpack
  2. 启动服务器 node server.js
  3. http://localhost:5555/

效果图片

文件缓存.gif

tree shaking 去除无用代码

使用前提

  1. 必须使用ES6模块化
  2. 开启production环境

优点: 在打包生产环境时,可以将我们未使用的代码进行忽略,从打包文件中删除我们未使用的代码,减小打包文件的体积。

tree shaking 只要我们满足前面的两个前提,webpack在打包时我自动进行无效代码的删除。

新增测试文件

/*
* testTreeShaking.js
*/
export const test1 = () => {
  console.log("test1");
};

export const test2 = () => {
  console.log("test2");
};
复制代码

修改文件

/*
* index.js
*/
...
import { test1 } from "./testTreeShaking";
test1();
// 未引入test2函数。打包时会忽略。
复制代码
/*
* webpack.config.js
*/
...
module.exports = {
...
// 修改打包模式
mode: 'production',
...
};
复制代码

package.json中配置

/*
* ackage.json
*/
"sideEffects": false 
// 没有副作用(都可以进行tree shaking)
// 可能会把css / @babel/polyfill (副作用)文件干掉
"sideEffects": ["*.css", "*.less"]
复制代码

webpack打包,查看打包文件如下图:

image.png

可以看到,test2函数并没有打包进入打包文件。

code Split 代码分割

代码分割分为三种:

  1. 多入口文件会自动进行代码分割
  2. optimization.splitChunks 控制代码分割
  3. optimization.splitChunks + import() 进行代码分割

优点: 进行代码分割可以有效拒绝js文件过于庞大。

多入口文件会自动进行代码分割

/*
* webpack.config.js
*/
...
module.exports = {
...
entry: {
    // 多入口:有一个入口,最终输出就有一个bundle
    index: "./src/index.js",
    test: "./src/testTreeShaking.js",
  },
...
};
复制代码

webpack打包查效果,如下图:

image.png

optimization.splitChunks控制代码分割

/*
* webpack.config.js
*/
...
module.exports = {
...
// 新增代码
optimization: {
    /*
    1. 可以将node_modules中代码单独打包一个chunk最终输出
    2. 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk
  */
    splitChunks: {
      //这表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 all,async 和 initial。
      //设置为 all 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享。
      chunks: "all",
    },
  },
...
};
复制代码

webpack打包查效果,如下图:

image.png

optimization.splitChunks + import() 进行代码分割

/*
* webpack.config.js
*/
...
module.exports = {
...
// 新增代码
optimization: {
    /*
    1. 可以将node_modules中代码单独打包一个chunk最终输出
    2. 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk
  */
    splitChunks: {
      //这表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 all,async 和 initial。
      //设置为 all 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享。
      chunks: "all",
    },
  },
...
};
复制代码
/*
* index.js
*/
/*
  通过js代码,让某个文件被单独打包成一个chunk
  import动态导入语法:能将某个文件单独打包
*/
import("./testTreeShaking").then(
  (res) => {
    console.log("res", res);
    res.test1();
  }
);

复制代码

webpack打包查效果,如下图:

image.png

文件懒加载 预加载

区别

  1. 使用文件的再加载
  2. 浏览器空闲时先进行加载

修改文件

/*
*index.html
*/
...
<button id="btn">加载testTreeShaking文件</button>
...
复制代码
/*
*index.js
*/
console.log("加载index文件");
复制代码
/*
*testTreeShaking.js
*/
export const test1 = () => {
  console.log("test1");
};

export const test2 = () => {
  console.log("test2");
};
console.log("加载index文件");
复制代码

文件懒加载

/*
*index.js
*/
console.log("加载index文件");
document.getElementById("btn").onclick = function () {
  // 懒加载:当文件需要使用时才加载
  import("./testTreeShaking").then(({ test1 }) => {
    test1();
  });
};
复制代码

懒加载效果图

懒加载.gif

预加载

/*
*index.js
*/
console.log("加载index文件");
document.getElementById("btn").onclick = function () {
  // 预加载 prefetch:会在使用之前,提前加载js文件
  // 正常加载可以认为是并行加载(同一时间加载多个文件)
  // 预加载 prefetch: webpackPrefetch 等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
  import(/* webpackPrefetch: true */ "./testTreeShaking").then(({ test1 }) => {
    test1();
  });
};
复制代码

预加载效果图

预加载.gif

PWA 渐进式网络应用程序

PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的。

淘宝的PWA效果图

添加 workbox-webpack-plugin 插件,然后调整 webpack.config.js 文件:

yarn add workbox-webpack-plugin --dev

修改webpack.config.js

/*
* webpack.config.js
*/
...
// 新增插件
const WorkboxWebpackPlugin = require("workbox-webpack-plugin");
module.exports = {
...
  // 插件
  plugins: [
    ...
    //新增插件
    new WorkboxWebpackPlugin.GenerateSW({
      /*
        1. 帮助serviceworker快速启动
        2. 删除旧的 serviceworker

        生成一个 serviceworker 配置文件~
      */
      clientsClaim: true,
      skipWaiting: true,
    }),
  ],
};
复制代码

注册 Service Worker

/*
* index.js
*/
/*
   sw代码必须运行在服务器上
      --> nodejs
      -->
        npm server 启动服务器,将build目录下所有资源作为静态资源暴露出去
*/
// 注册serviceWorker
// 处理兼容性问题
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker
      .register("/service-worker.js")
      .then(() => {
        console.log("sw注册成功了~");
      })
      .catch(() => {
        console.log("sw注册失败了~");
      });
  });
}
复制代码

接下来我们将之前的server.js文件复制过来直接来使用:

  1. 先进行打包
  2. node server.js 启动服务器
  3. http://localhost:5555/

检查PWA效果图片

PWA.gif

多进程打包

yarn add thread-loader --dev

修改webpack.config.js文件:

/*
* webpack.config

.js
*/
...
module.exports = {
...
  // 模块
  module: {
    rules: [
      // loader的配置
      {
        oneOf: [
         ...
         // 新增js的loader
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              /* 
                新增代码
                开启多进程打包。 
                进程启动大概为600ms,进程通信也有开销。
                只有工作消耗时间比较长,才需要多进程打包
              */
              {
                loader: "thread-loader",
                options: {
                  // 产生的 worker 的数量,默认是 (cpu 核心数 - 1),或者,
                  // 在 require('os').cpus() 是 undefined 时回退至 1
                  workers: 2, // 进程2个
                },
              },
              ...
            ],
          },
        ],
      },
    ],
  },
...
};
复制代码

运行webpack可以查看效果,因为这个多进程打包启动需要600ms,所以比较适合代码体量大一点的项目。

未开启多进程打包效果图

image.png

开启多进程打包效果图

image.png

externals

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

修改webpack.config.js文件,添加externals属性

/*
* webpack.config.js
*/
...
module.exports = {
...
  externals: {
    jquery: "jQuery",
  },
};
复制代码

选择JQ的CDN链接,添加到index.html

免费CDN地址

/*
* index.html
*/ 
<!DOCTYPE html>
<html lang="zn">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>webpack打包优化</title>
  <!-- 新增代码 -->
  <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
</head>

<body>
  <h1>webpack 打包优化</h1>
  <div class="color_1"></div>
  <div class="color_2"></div>
  <img src="./img/zp.jpg" />
</body>

</html>
复制代码
/*
* index.js
*/
import $ from "jquery";
console.log("$", $);
复制代码

webpack打包进行测试。

效果图片

外部扩展.gif

Dll 动态链接库

实现了拆分 bundles,同时还大幅度提升了构建的速度。"DLL" 一词代表微软最初引入的动态链接库。

新增webpack.dll.js文件,用于单独配置DllPlugin。

/*
* webpack.dll.js
*/
const { resolve } = require("path");
const webpack = require("webpack");

module.exports = {
  entry: {
    // 最终打包生成的[name] --> dllFile
    // ['jquery',"lodash"] --> 要打包的库是jquery lodash
    dllFile: ["jquery", "lodash"],
  },
  output: {
    filename: "[name].js",
    path: resolve(__dirname, "dll"),
    library: "[name]_[hash]", // 打包的库里面向外暴露出去的内容叫什么名字
  },
  plugins: [
    // 打包生成一个 manifest.json --> 提供和jquery lodash映射
    new webpack.DllPlugin({
      name: "[name]_[hash]", // 映射库的暴露的内容名称
      path: resolve(__dirname, "dll/manifest.json"), // 输出文件路径
    }),
  ],
  mode: "production",
};
复制代码

运行 webpack --config webpack.dll.js 执行该文件,生成如下文件:

image.png

修改webpack.config.js文件 配置 DllReferencePlugin AddAssetHtmlWebpackPlugin。

下载 yarn add --dev add-asset-html-webpack-plugin

/*
* webpack.config.js
*/
...
// 新增代码
const webpack = require("webpack");
const AddAssetHtmlWebpackPlugin = require("add-asset-html-webpack-plugin");
module.exports = {
...
  plugins: [
   ...
     // 新增代码
    // 告诉webpack哪些库不参与打包,同时使用时的名称也得变~
    new webpack.DllReferencePlugin({
      manifest: resolve(__dirname, "dll/manifest.json"),
    }),
    // 将某个文件打包输出去,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
      filepath: resolve(__dirname, "dll/dllFile.js"),
      outputPath: "dll", // 如果设置,将用作文件的输出目录。
      publicPath: "dll", // 如果设置,将用作脚本或链接标记的公共路径。
    }),
  ],
};
复制代码

修改index.js 文件 进行测试:

/*
* index.js
*/
...
import $ from "jquery";
console.log("$--->jquery", $);
import _ from "lodash";
console.log("_---->lodash", _);
复制代码

运行 webpack 进行打包。生成如下文件:

image.png

查看运行效果

屏幕录制2021-05-14 11.gif

webpack打包优化理论篇至此结束,其实关于打包还有很多点,欢迎大家评论区进行评论,我们共同成长!!

想查看源码的同学点击这里获取

下一篇 打包实践篇。

文章分类
前端
文章标签