专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
前端早读课  ·  【招聘】北京Dine团队招聘前端工程师 ·  5 天前  
前端早读课  ·  【第3376期】Ling(灵):追求极致响应 ... ·  5 天前  
前端大全  ·  新的 JS 提案让你告别 try catch ! ·  5 天前  
前端早读课  ·  【第3375期】WebRTC ... ·  6 天前  
51好读  ›  专栏  ›  前端大全

晋升必备:Webpack 性能优化方案看这篇就够了!

前端大全  · 公众号  · 前端  · 2024-09-10 10:10

正文

作者:文学与代码

https://juejin.cn/post/7395969637878693942

无论在面试还是内部晋升,性能优化方案 一直都是非常重要的部分。

性能优化可以分为很多种,比如:

  1. 打包工具(webpack || vite)性能优化
  2. 访问速度优化
  3. 用户感知优化
  4. 代码标准化
  5. ...

在了解 webpack 的性能优化之前,我们需要先对性能优化有一个总体的认知,总共分为 3 点:

  1. 在业务开发中,不要过早的考虑性能优化,而且业务开发大部分时候都不需要进行性能优化。因为 vue-cli 这些脚手架已经帮助业务开发者进行了最佳实践。
  2. 性能优化没有一招鲜吃遍天的方法论,而是因地制宜,见招拆招,所以任何性能优化的方案都不要死记硬背,而是理解其本质原理。
  3. 性能优化其实主要就是优化以下3个方面:
    1. 优化构建效率。
    2. 优化网络传输效率。
    3. 优化代码执行效率。而 webpack 层面主要优化的是前两种效率。

本文是 webpack 性能优化相关文章的第一篇,将从以下几个角度开始探讨:

  1. webpack 基本原理
  2. 搭建基础 webpack5 调试环境
  3. 分析 webpack dev模式打包结果
  4. 分析 webpack prod模式打包结果
  5. 去除掉重复打包模块以及没有使用的模块
  6. 自动分包的实施方案

webpack 基本原理

webpack打包构建的核心流程可以总结为以下的流程图:

image.png

webpack在打包流程的各个阶段都设置了勾子函数,基于这些勾子函数,开发者可以自定义webpack插件,来实现一些强大的自定义功能。在编译过程中,webpack会对各个模块的导出内容进行缓存,如果在这个过程中,webpack发现这个模块已经存在了缓存,那么就不会重复处理这个模块了。

搭建最基础的webpack实验环境:

我们不基于vue-cli,而是直接搭建一个基于 webpack5 的vue3开发环境:

首先我们可以直接安装以下以下依赖:


  "devDependencies": {
    "@babel/core""^7.24.7",
    "@babel/plugin-transform-runtime""^7.24.7",
    "@babel/preset-env""^7.24.7",
    "@babel/preset-typescript""^7.24.7",
    "@types/lodash""^4.17.7",
    "@types/node""^20.14.8",
    "babel-loader""^9.1.3",
    "copy-webpack-plugin""^12.0.2",
    "cross-env""^7.0.3",
    "css-loader""^7.1.2",
    "dotenv""^16.4.5",
    "html-webpack-plugin""^5.6.0",
    "mini-css-extract-plugin""^2.9.0",
    "postcss""^8.4.38",
    "postcss-loader""^8.1.1",
    "postcss-preset-env""^9.5.14",
    "regenerator-runtime""^0.14.1",
    "sass""^1.77.6",
    "sass-loader""^14.2.1",
    "serve""^14.2.3",
    "style-loader""^4.0.0",
    "style-resources-loader""^1.5.0",
    "ts-loader""^9.5.1",
    "vue-loader""^17.4.2",
    "webpack""^5.92.1",
    "webpack-bundle-analyzer""^4.10.2",
    "webpack-cli""^5.1.4",
    "webpack-dev-server""^5.0.4",
    "webpack-merge""^5.10.0"
  },
  "dependencies": {
    "@babel/runtime""^7.24.7",
    "@babel/runtime-corejs3""^7.24.7",
    "core-js""^3.37.1",
    "element-plus""^2.7.8",
    "lodash""^4.17.21",
    "lodash-es""^4.17.21",
    "vue""^3.4.29",
    "vue-router""^4.4.0"
  }

在目录下新建一个 webpack 的文件夹,在里面新建一个 base-webpack 配置以及分别用于构建开发环境以及生产环境的配置文件:

image.png

我们在webpack-base文件中加入如下配置:


// 公共的 webpack 配置

const { relative, resolve } = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");
const webpack = require("webpack");
const MinicssExtractPlugin = require("mini-css-extract-plugin");
const setCssRules = require("./setCssRules");
const setModuleCssRule = require("./setModuleCssRule");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

// 读取 node_env 环境变量的值

let nodeEnv = process.env.NODE_ENV;

if (!nodeEnv) {
  nodeEnv = "development";
}

const isProd = nodeEnv === "production";

const envVars = [
  ".env",
  `.env.${nodeEnv}`,
  `.env.${nodeEnv}.local`,
  ".env.local",
].filter(Boolean);

// 读取当前构建环境对应的环境变量文件的所有内容,将其注入到环境变量中

envVars.forEach((envVar) => {
  const envFilePath = resolve(__dirname, "..", envVar);
  const envFileExists = require("fs").existsSync(envFilePath);
  if (envFileExists) {
    require("dotenv").config({
      path: envFilePath,
    });
  }
});

/**
 * @type {import('webpack').Configuration}
 */


module.exports = {
  // 配置入口
  entry: resolve(__dirname, "..""src""main.ts"),
  optimization: {
    // 暂时不要压缩代码
    minimizetrue,
  },
  // 配置打包出口
  output: {
    path: resolve(__dirname, "..""dist"),
    // 使用文件指纹
    filename"js/[name].[contenthash:6].js",
    // 从 webpack5 开始,只要开启这个开关,那么每一次构建会自动清理输出目录
    cleantrue,
    // 打包后访问的资源前缀
    publicPath"/",
  },
  // 配置路径别名
  resolve: {
    alias: {
      "@": resolve(__dirname, "..""src"),
      vue$"vue/dist/vue.runtime.esm-bundler.js",
    },
    // 配置模块的访问路径
    extensions: [".js"".ts"".tsx"".vue"".json"],
  },
  // 配置插件
  plugins: [
    new HtmlWebpackPlugin({
      // 指定 html 模板的路径
      template: resolve(__dirname, "..""public""index.html"),
      // 该配置会注入到 html 文件的模板语法中
      title: process.env.VUE_APP_TITLE,
    }),
    // 加载 vue-loader 插件
    new VueLoaderPlugin(),
    // 在编译时候全局替换静态值
    new webpack.DefinePlugin({
      // 定义全局变量
      "process.env": {
        VUE_APP_API_URIJSON.stringify(process.env.VUE_APP_API_URI),
      },
      // 决定 vue3 是否启用 options api
      __VUE_OPTIONS_API__true,
      // Vue Devtools 在生产环境中不可用
      __VUE_PROD_DEVTOOLS__false,
    }),
    new MinicssExtractPlugin({
      filename"css/[name].[contenthash:6].css",
      // chunkFilename: "css/[name].[contenthash:6].css",
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: resolve(__dirname, "..""public"),
          to: resolve(__dirname, "..""dist"),
          toType"dir",
          globOptions: {
            ignore: ["**/index.html""**/.DS_Store"],
          },
          info: {
            minimizedtrue// 注意:minimize 应该是 minimized,根据CopyWebpackPlugin的文档进行修正
          },
        },
      ],
    }),
    new BundleAnalyzerPlugin({
      analyzerMode'server',
      analyzerHost'127.0.0.1',
      analyzerPort8888,
      openAnalyzertrue,
      generateStatsFilefalse,
      statsOptionsnull,
      logLevel'info'
    })
  ],
  module: {
    // 配置 leader
    rules: [
      // 配置 js loader
      {
        test/\.js$/,
        use"babel-loader",
        exclude/node_modules/,
      },
      {
        // 配置 .vue 文件
        test/\.vue$/,
        use"vue-loader",
      },
      // 匹配 .ts(x)
      {
        test/\.tsx?$/,
        // 先暴力排除 node_modules 目录
        exclude/node_modules/,
        use: [
          {
            loader"ts-loader",
            options: {
              // 在编译 ts 的时候关闭类型检查,只进行代码转换
              transpileOnlytrue,
              // .vue 文件在编译过程中添加 .ts 后缀
              appendTsSuffixTo: [/\.vue$/],
            },
          },
          // 在 ts 处理完成之后,将内容交给 babel-loader 处理
          {
            loader"babel-loader",
            // options: {
            //   // 添加 babel 预设
            //   presets: [
            //     [
            //       "@babel/preset-typescript",
            //       {
            //         // 尝试转换任意类型文件中的 ts 代码
            //         allExtensions: true,
            //       },
            //     ],
            //   ],
            // },
          },
        ],
      },
      {
        oneOf: [
          // 处理 css 相关的内容
          {
            test/\.css$/,
            // 过滤掉 node_modules 以及以 .module.css 结尾的文件
            exclude: [/\.module\.css$/],
            use: setCssRules("css", isProd),
          },
          // 处理 scss 相关的内容
          {
            test/\.s[ac]ss$/i,
            // 过滤掉 node_modules 以及以 .module.scss 结尾的文件
            exclude: [/\.module\.s[ac]ss$/],
            use: setCssRules("scss", isProd),
          },
          //  处理 .module.css 结尾的文件
          {
            test/\.module\.css$/,
            exclude/node_modules/,
            use: setModuleCssRule("css", isProd),
          },
          // 处理 .module.scss 结尾的文件
          {
            test/\.module\.s[ac]ss$/,
            exclude/node_modules/,
            use: setModuleCssRule("scss", isProd),
          },
        ],
      },
      // webpack5处理图片相关的静态资源
      {
        test/\.(png|jpe?g|gif|webp|avif|svg)(\?.*)?$/,
        // 使用 webpack5 覅人新特性,不再需要使用loader去进行处理
        // 而且 assets 是 webpack5 通用的资源处理类型
        // 默认情况下 8kb 以下的资源会被转化为 base64 编码
        type"asset",
        parser: {
          dataUrlCondition: {
            // 自定义 10 kb 以下的资源会被转化为 base 64 位编码
            maxSize10 * 1024,
          },
        },
        generator: {
          // 输出图片的目录
          // outputPath: "images",
          // 输出图片的名称
          filename"images/[name].[contenthash:6].[ext]",
        },
      },
      // svg 类型的静态资源期望转为为 asset/resource 类型进行处理
      {
        test/\.(svg)(\?.*)?$/,
        // 默认会将构建结果导出单独的配置文件
        type"asset/resource",
        generator: {
          // 输出 svg 的目录
          // outputPath: "images",
          // 输出 svg 的名称
          filename"svgs/[name].[contenthash:6].[ext]",
        },
      },
    ],
  },
};


配置比较简单,就是搭建 vue3 开发环境的基础配置。配置完成之后,我们就继续编写 dev 环境构建配置以及生产环境构建配置:


const baseConfig = require('./webpack.base.js')
const { merge } = require('webpack-merge')

/**
 * @type {import('webpack').Configuration}
 */


const devConfig = {
    mode"development",
    // webpack5 本身就直接支持了热更新
    // 定义sourceMap
    devtool"cheap-module-source-map",
    devServer: {
        portNumber(process.env.VUE_APP_PORT),
        // 启动完成之后自动打开
        openJSON.parse(process.env.VUE_APP_OPEN),
        // 在访问资源 404 之后 自动导航到 index.html
        historyApiFallbacktrue,
    }
}

module.exports = merge(baseConfig, devConfig)


const baseConfig = require('./webpack.base.js')
const { merge } = require('webpack-merge')

/**
 * @type {import('webpack').Configuration}
 */


const prodConfig = {
  mode'production',
}

module.exports = merge(baseConfig, prodConfig)


配置完毕之后我们可以加上如下的script脚本命令:


  "scripts": {
    "build:dev""webpack -c webpack/webpack.dev.js --node-env devlopment",
    "build:prod""webpack -c webpack/webpack.prod.js --node-env production",
    "priview""serve -s dist -l 4000",
    "serve""webpack-dev-server -c webpack/webpack.dev.js --node-env devlopment"
  },

接着我们可以配置vue3的业务代码:

首先配置入口文件:


// src/main.ts

import { createApp, h } from "vue"
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from "./App.vue"
import './style.css'
import router from './router'

createApp(App).use(ElementPlus).use(router).mount("#app")

紧接着我们配置 App.vue 入口组件:


我们配置 /router/index.js 文件:


// 生成vue-router的初始化配置代码

import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';

const routes = [
  {
    path'/',
    name'Home',
    component: Home,
  },
  {
    path'/about',
    name'About',
    component: About,
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL || '/'),
  routes,
});

export default router;

我们新建 views 页面文件:


home.vue