Skip to content

优化代码运行性能

Code Split

打包代码时会将所有 js 文件打包到一个文件中,体积太大。所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源变少,速度就更快

  • 分割文件:将打包生成的文件进行分割,生成多个 js 文件
  • 按需加载:需要哪个文件就加载哪个文件

一、 多入口

1. 文件目录

text
├── public
├── src
|   ├── app.js
|   └── main.js
├── package.json
└── webpack.config.js

2. 配置

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

module.exports = {
  /* 单入口 */
  // entry:'./src/main.js',
  /* 多入口 */
  entry: {
    main: "./src/main.js",
    app: "./src.app.js",
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
    // 打包的资源就是chunk,输出出去叫bundle。
    // 此处chunk的 name 为多入口定义的 app 和 main,但与文件名无关
    filename: "js/[name].js",
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "./public/index.html"),
    }),
  ],
  //...
};

3. 输出

dist/js 目录下会输出两个 js 文件;

配置了几个入口,至少输出几个 js 文件

二、提取重复代码

如果多入口文件都引用了同一份代码,会分别打包到各自输出文件中,致使包体积变大。所以可以提取出多入口中重复的代码,打包成单独的 js 文件,然后直接引用

  • math.js
js
export const sum = (a, b) => {
  return a + b;
};
  • main.js
js
import { sum } from "./math.js";
sum(1, 2);
  • app.js
js
import { sum } from "./math.js";
sum(3, 4);

1. 修改配置文件

  • splitChunks 的默认值:
js
splitChunks: {
    chunks: 'async',
    minSize: 20000, //生成 chunk 的最小体积(以 bytes 为单位)
    minRemainingSize: 0, // 类似minSize,最后确保提取的文件大小不能为0
    minChunks: 1, //至少被引用的次数,满足条件才会代码分割
    maxAsyncRequests: 30, // 按需加载时的最大并行请求数
    maxInitialRequests: 30, //入口点的最大并行请求数
    enforceSizeThreshold: 50000, //强制执行拆分的体积阈值(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略
    //  组,哪些模块要打包到一个组
    cacheGroups: {
        // 组名
        defaultVendors: {
        test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
        priority: -10, // 权重(越大越高)
        // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
        reuseExistingChunk: true,
        },
        // 此处定义的属性会替换上面相应的默认属性
        default: {
        minChunks: 2,
        priority: -20, // 权重(越大优先执行)
        reuseExistingChunk: true,
        },
    },
}
  • 项目中配置:
js
const path = require("path");

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

三、按需加载,动态导入

1. 基础配置

js
// main.js

// 动态导入 --> 实现按需加载
// 即使只被引用了一次,也会代码分割
document.getElementById("btn").onclick = () => {
  import("./math.js").then(({ sum }) => {
    sum(1, 2);
  });
};

2. 动态导入命名

eslint 会对动态导入语法报错,需要修改 eslint 配置文件

  • main.js
js
document.getElementById("btn").onclick = () => {
  // webpackChunkName: "math":这是webpack动态导入模块命名的方式
  // "math"将来就会作为[name]的值显示。
  import(/* webpackChunkName: "math" */ "./js/math.js").then(({ sum }) => {
    sum(1, 2);
  });
};
  • eslint 配置
shell
npm i eslint-plugin-import -D
js
// .eslintrc.js
module.exports = {
  extends: ["eslint:recommended"],
  env: {
    node: true, // 启用node中全局变量
    browser: true, // 启用浏览器中全局变量
  },
  // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
  plugins: ["import"],
  parserOptions: {
    ecmaVersion: 6,
    sourceType: "module",
  },
  rules: {
    "no-var": 2, // 不能使用 var 定义变量
  },
};

Preload/Prefetch

预获取/预加载后续需要用到的资源并缓存,提升浏览体验

1. 定义

  • prefetch(预获取):将来某些导航下可能需要的资源

  • preload(预加载):当前导航下可能需要资源

  • 共同点

    1. 只加载资源,并不执行
    2. 都会有缓存
    3. 兼容性差,prefetch 相对更差些
  • 区别

    1. preload 优先级更高,与父chunk并行加载,prefetch 在父chunk加载结束后加载
    2. preload 立即加载,prefetch 在浏览器空闲时加载

2. 使用

shell
npm i @vue/preload-webpack-plugin -D
js
// webpack.config.js
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
module.exports = {
  //...
  plugins: [
    new PreloadWebpackPlugin({
      //   rel: "preload",
      rel: "prefetch",
      as: "script",
    }),
  ],
  //...
};

打包后输出:

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 其他略 -->
    <link rel="preload" as="script" href="static/js/math.chunk.js" />
  </head>
  <body>
    <div>hello webpack</div>
  </body>
</html>

Network Cache

浏览器会缓存静态资源在本地,当第二次加载时可以读取缓存资源,减少请求提升运行速度

当更改一个文件内代码时,所输出的文件名是不会改变的,这样会导致因文件名相同,浏览器缓存之前的资源,而没有加载最新的资源文件。所以,需要对文件名进行 hash 值操作


  • fullhash(webpack4 为 hash)
    所有输出文件 hash 值都相同,修改任何一个文件,所有文件 hash 值都将改变,导致所有文件缓存失效要重新加载
  • chunkhash
    根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。
  • contenthash
    在 js 文件中导入 CSS 文件,因是同一个入口文件,会共享一个 hash 值。只改动 JS 代码,CSS 文件的 hash 也会跟着变,这个时候就需要用 contenthash

三种 hash 具体区别

1. 使用

js
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    // 入口文件打包命名
    filename: "./static/js/[name].[chunkhash:8].js",
    // 动态导入输出文件
    chunkFilename: "./static/js/[name].[chunkhash:8].chunk.js",
    clean: true,
  },
  //...
  plugins: [
    // 提取css成单独文件
    new MiniCssExtractPlugin({
      filename: "static/css/[name].[contenthash:8].css",
      chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
    }),
  ],
  //...
};

2. runtimeChunk

将包含 chunks 映射关系的 list 单独从 main.js里提取出来,单独生成一个 runtime~xxx.js 的文件。避免每次修改文件,main.js的 hash 值都会改变,导致缓存失效

js
// webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: all,
    },
    // 提取runtime文件
    // runtimeChunk: true,
    runtimeChunk: {
      // 自定义runtime文件命名规则
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  },
  //...
};

Core-js

core-js 是专门用来做 ES6 以及以上 API 的 polyfill

一般用 babel 对 js 代码进行兼容性处理,其中使用 @babel/preset-env 智能预设来处理兼容性问题。它能将 ES6 的一些语法进行编译转换,比如箭头函数、...运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。

1. 配置 eslint

js
// main.js

//...
// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
  console.log("hello promise");
});

此时eslint会对promise报错

  • 配置 eslint
shell
npm i @babel/eslint-parser -D
js
// .eslintrc.js
module.exports = {
  // 继承 Eslint 规则
  extends: ["eslint:recommended"],
  parser: "@babel/eslint-parser", // 支持最新的最终 ECMAScript 标准
  env: {
    node: true, // 启用node中全局变量
    browser: true, // 启用浏览器中全局变量
  },
  plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
  parserOptions: {
    ecmaVersion: 6, // es6
    sourceType: "module", // es module
  },
  rules: {
    "no-var": 2, // 不能使用 var 定义变量
  },
};

打包输出 js 文件, Promise 语法并没有编译转换,需要使用 core-js 来进行 polyfill

2. 手动引入

shell
npm i core-js
js
// main.js
// import "core-js"; //引入全部
import "core-js/es/promise"; //按需引入
// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
  console.log("hello promise");
});

3. 自动按需引入

  • babel.config.js
js
module.exports = {
  // 智能预设:能够编译ES6语法
  presets: [
    [
      "@babel/preset-env",
      // 按需加载core-js的polyfill
      { useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
    ],
  ],
};

PWA

渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。
其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。内部通过 Service Workers 技术实现的。

使用

shell
npm i workbox-webpack-plugin -D
  • webpack.config.js
js
const WorkboxPlugin = require("workbox-webpack-plugin");
//...
plugins: [
  new WorkboxPlugin.GenerateSW({
    // 这些选项帮助快速启用 ServiceWorkers
    // 不允许遗留任何“旧的” ServiceWorkers
    clientsClaim: true,
    skipWaiting: true,
  }),
];
//...
  • main.js
js
//...
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker
      .register("/service-worker.js")
      .then((registration) => {
        console.log("SW registered: ", registration);
      })
      .catch((registrationError) => {
        console.log("SW registration failed: ", registrationError);
      });
  });
}

运行

此时如果直接通过 VSCode 访问打包后页面,在浏览器控制台会发现 SW registration failed 因为打开的访问路径是:http://127.0.0.1:5500/dist/index.html。此时页面会去请求 service-worker.js 文件,请求路径是:http://127.0.0.1:5500/service-worker.js,这样找不到会 404。

实际 service-worker.js 文件路径是:http://127.0.0.1:5500/dist/service-worker.js

  • 解决路径问题
shell
npm i serve -g

运行指令:

shell
serve dist

此时通过 serve 启动的服务器, service-worker 就能注册成功