⚡性能优化实践(主要从网络层面和构建层面进行优化)

OJ项目存在问题

网站初次载入时间长

明显的看到:chunk-vendors.js 文件过大导致页面加载缓慢

chunk-vendors.js是啥

chunk-vendors.js,顾名思义chunk(块/包)-vendors(供应商),即为:不是自己写的模块包,也就是/node_modules项目目录的所有模块包。

为什么会过大

我们把第三方的包都打包在这一个文件上了,文件这么大,加载就慢了。

做题页面

管理题目页面

这里看不出来是什么文件导致的加载缓慢,所以让我们上分析工具进行分析

分析手段

关于一些性能指标和工具介绍可以看一下:

【前端工程化-性能优化】性能优化系列之用户体验的问题分析与定位 - 掘金

Lighthouse

webpack-bundle-analyse 包体积分析

如果是vuecli 3的话,先安装插件

pnpm install webpack-bundle-analyzer --save-dev

然后在vue.config.js中对webpack进行配置

arduino 复制代码
chainWebpack: (config) => {
  /* 添加分析工具*/
  if (process.env.NODE_ENV === 'production') {
    if (process.env.npm_config_report) {
      config
        .plugin('webpack-bundle-analyzer')
        .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
        .end();
      config.plugins.delete('prefetch')
    }
  } 
}

再运行pnpm run build --report

会在浏览器打开一个项目打包的情况图,便于直观地比较各个bundle文件的大小

分析结果:

  • 整体打包体积 35.9 MB
  • ts.worker.js 是 monaco 编辑器的语言支持文件,主要提供 typescript 语法支持,体积 7.88 MB
  • chunk-vendors.js 第三方模块捆绑包,体积 3.41 MB
  • html.workar.js 是 monaco 编辑器的 html 语法支持文件
  • css.workar.js 是 monaco 编辑器的 css 语法支持文件
  • json.worker.js 是 monaco 编辑器的 json 语法支持文件
  • 小文件文件数量太多,加起来接近百个,导致 http 请求过多

经过分析总结,定位问题如下:

  1. monaco-editor 是最大的问题,体积大,严重影响加载速度,需要优化 (做题页面和管理题目页面都有这个组件,难怪加载这么慢)
  2. chunk-vendors.js 作为公共模块,构成项目必不可少的一些基础类库,升级频率都不高,但每个页面都需要它们,现在它体积过大,应该在合理范围内拆分成更小一些的 js,以利用浏览器的并发请求,优化首页加载体验。其中包含了三个大家伙:arco-design (2.76MB)、moment(563.86KB) 和 highlight(1.3MB),更是需要单独做优化
  3. 小文件数量太多,需要合并

动态测量------常用的性能测量API

计算可交互时间:

ini 复制代码
window.addEventListener('load', function() {
  // Time to Interacrtive 可交互时间
  let timing = performance.getEntriesByType('navigation')[0];
  // 计算 tti = domInteractive - fetchStart (注意:单位是毫秒)
  let tti = timing.domInteractive - timing.fetchStart;
});

相关时间节点计算总结

  • DNS 解析耗时: domainLookupEnd - domainLookupStart
  • TCP 连接耗时: connectEnd - connectStart
  • SSL 连接耗时: connectEnd - secureConnectionStart
  • 网络请求耗时 (TTFB): responseStart - requestStart
  • 数据传输耗时: responseEnd - responseStart
  • DOM 解析耗时: domInteractive - responseEnd
  • 资源加载耗时: loadEventStart - domContentLoadedEventEnd
  • FirstByte时间: responseStart - domainLookupStart
  • 白屏时间: responseEnd - fetchStart
  • 首次可交互时间: domInteractive - fetchStart
  • DOMReady 时间: domContentLoadEventEnd - fetchStart
  • 页面完全加载时间: loadEventStart - fetchStart
  • http 头部大小: transferSize - encodedBodySize
  • 重定向次数:performance.navigation.redirectCount
  • 重定向耗时: redirectEnd - redirectStart

开始优化

优化 monaco-editor

单独打包

因为monaco-editor 作为一个重量级组件,会分散很多小文件到各个地方,从而增加文件数量和体积,进而造成流量损失,通过将monaco-editor拆分为独立文件,并充分利用浏览器缓存,您可以显著提升网站性能,减少重复加载的资源消耗。

less 复制代码
splitChunks: {
    chunks: 'all', //所有类型的chunks(同步和异步)都将被考虑进行分割
    minSize: 20000, //分割出的chunks的最小大小为20KB。小于这个大小的chunks不会被分割。
    maxAsyncRequests: 30, 
    maxInitialRequests: 30, //控制加载初始页面所需的请求数量,
    enforceSizeThreshold: 50000,
    maxSize: 0,
    cacheGroups: {
      monacoEditor: {
        chunks: 'async',
        name: 'chunk-monaco-editor',
        priority: 22,
        test: /[\/]node_modules[\/]monaco-editor[\/]/,
        enforce: true,
        reuseExistingChunk: true,
      },
  },

缩小体积

webpack配置

javascript 复制代码
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

module.exports=function(){
  return {
    ...
    plugins:[
      new MonacoWebpackPlugin()
    ]
    ...
  }
}

插件会帮我们做这么几件事

  1. 自动注入getWorkerUrl全局变量
  2. 处理worker的编译配置
  3. 自动引入控件和语言包。

具体要引入哪些控件和语言包,我们可以通过配置languagesfeatures来控制

arduino 复制代码
new MonacoWebpackPlugin({
 languages:["javascript","css","html","json"],
 features:["coreCommands","find"]
})

缺省情况下,插件的会引入所有的语言包和控件, 配置添加插件选项 languages 为支持的语言数组(具体语言查看官网)默认是支持所有语言的,配置此项应该只是去除一些语言的高级特性支持。

按需引入

打包构建部署到服务器上,很明显地发现影响做题页面地加载缓慢地因素是:monaco-editor

管理题目还能用组件懒加载去解决,但是做题页面一点击去就呈现了,那就按需引入,让它加载地资源少一点

import * as monaco from "monaco-editor";这段代码引入了整个代码包;

因为我们用不到那么多的功能,可以根据相关的功能选择性的引入

方案二: 更换更小的库

vue-codemirror

使用:

页面呈现:

方案三:@monaco-editor/loader (推荐使用)

既然按需引入不知道如何删减,但是很明显monaco-editor的存在导致加载缓慢,那就看看leetcode是怎么做的吧:

用的是cdn!!!

还看了其他的网站(牛客、lintcode等)差不多都是将这个放到自己的cdn服务里面,再引用 CDN 上的资源链接

然后在查找资料发现了: @monaco-editor/loader

使用 @monaco-editor/loader ,下载的文件在 1M以下,并使用了 jsdelivr 的CDN加速服务,可以使用到 monaco-editor 的几乎所有功能。

使用:

  1. 下载:pnpm install @monaco-editor/loader
  2. 创建hooks:

由于这是一部分可以公用的代码,所以使用了 vue3 的 hooks,方便 monaco 的功能可重复地使用。

创建了 initMonaco 方法异步引入 monaco,并避免重复初始化,并且不阻塞页面的正常加载,提供了一个monacoRef(即为前文中的 monaco),在想使用的页面中使用。

typescript 复制代码
// src\hooks\useMonacoEditor.ts
import loader from "@monaco-editor/loader";
import { ref } from "vue";

loader.config({
  paths: {
    vs: "https://cdn.staticfile.org/monaco-editor/0.43.0/min/vs", // 国内地第三方cdn服务
  },
});
const monacoRef = ref<any>(null);
const isLoading = ref<boolean>(false); // 添加一个状态来表示是否正在加载
const monacoLoader = loader.init();

const initMonaco = () => {
  return new Promise<void>((resolve, reject) => {
    if (monacoRef.value) {
      resolve();
      return;
    }
    isLoading.value = true; // 开始加载
    monacoLoader
      .then((monacoInstance) => {
        monacoRef.value = monacoInstance;
        isLoading.value = false; // 加载完成
        resolve();
      })
      .catch((error) => {
        isLoading.value = false; // 加载失败
        if (error?.type !== "cancelation") {
          console.error("Monaco initialization error:", error);
          reject();
        }
      });
  });
};

export function useMonacoEditor() {
  return {
    initMonaco,
    monacoRef,
    isLoading,
  };
}
  1. 使用
xml 复制代码
<template>
  <a-spin :loading="isLoading" tip="加载中,请稍后...">
    <div id="code-editor" ref="codeEditorRef" />
  </a-spin>
</template>

<script setup lang="ts">
  import { useMonacoEditor } from "@/hooks/useMonacoEditor";
  const { monacoRef, initMonaco, isLoading } = useMonacoEditor();

  ...
  onMounted(async () => {
    if (!codeEditorRef.value) {
      return;
    }
    await initMonaco();

    codeEditor.value = monacoRef.value.editor.create(codeEditorRef.value, {
      value: props.value, //编辑器初始显示文字
      language: props.language, //语言支持自行查阅demo
      ...
    });

    // 编辑 监听内容变化
    codeEditor.value.onDidChangeModelContent(() => {
      props.handleChange(toRaw(codeEditor.value).getValue()); //getValue()则是用于获取编辑器实例的内容
    });
    if (theme.value === "light") {
      themeChange("vs");
    } else {
      themeChange("vs-dark");
    }
  });
</script>

<style scoped>
  #code-editor {
    height: 100%;
  }
</style>

优化arco-design

单独打包

less 复制代码
configureWebpack: (config) => {
    let optimization = {
      splitChunks: {
        ...
        cacheGroups: {
          ...
          arcoDesignUI: {
            chunks: "all",
            name: "chunk-arco-design",
            priority: 21,
            test: /[\/]node_modules[\/]@arco-design[\/]/,
            enforce: true,
            reuseExistingChunk: true,
          },
        },
      },
    };
    Object.assign(config, {
      optimization,
    });
  },

按需引入

官方文档: Arco Design Vue

按需加载

安装: unplugin-auto-import/webpack 和 unplugin-vue-components/resolvers

php 复制代码
const Components = require("unplugin-vue-components/webpack");
const AutoImport = require("unplugin-auto-import/webpack");
const { ArcoResolver } = require("unplugin-vue-components/resolvers");
module.exports = defineConfig({
configureWebpack: {
    plugins: [
        Components({
          resolvers: [
            ArcoResolver({
              resolveIcons: true,
              sideEffect: true,
            }),
          ],
        }),
        AutoImport({
          resolvers: [ArcoResolver()],
        }),
      ],
  }
}

对于在script 中手动导入的组件,比如 Message 组件,还需要手动导入组件对应的样式文件:

javascript 复制代码
import { Message } from "@arco-design/web-vue";
import "@arco-design/web-vue/es/message/style/css.js";

优化moment

减小三方依赖的体积

项目中使用了 momentjs,发现打包后有很多没有用到的语言包(红色框框就是我们需要的zh-cn)

使用 moment-locales-webpack-plugin 插件,剔除掉无用的语言包

  1. 安装:pnpm install moment-locales-webpack-plugin -D
  2. vue.config.js引入
arduino 复制代码
chainWebpack: (config) => {
    ...
    config.plugin("moment").use(
      new MomentLocalesPlugin({
        localesToKeep: ["zh-cn"],
      })
    );
}

moment 包体积:563.86KB => 144.85KB

更换更小的库

moment => dayjs

144.85KB => 12.34KB

优化bytemd

在上面优化过后,性能有了很大的提升(主要是因为cdn)但是网页的加载还是长达6秒之久,而且只知道是chunk_380、chunk_597、chunk_508这个东西,不知道具体是什么,webpack-bundle-analyse 也找不到,所以我换了vue-cli自带的分析工具:vue ui,直接在终端输入vue ui 进行使用

单独分包

php 复制代码
module.exports = defineConfig({
  cacheGroups: {
          bytemd: {
            chunks: "all",
            name: "chunk-bytemd",
            priority: 22,
            test: /[\/]node_modules[\/]@bytemd[\/]/,
            enforce: true,
            reuseExistingChunk: true,
          },
          "@bytemd": {
            chunks: "all",
            name: "chunk-@bytemd",
            priority: 21,
            test: /[\/]node_modules[\/]bytemd[\/]/,
            enforce: true,
            reuseExistingChunk: true,
          },
          codemirror: {
            chunks: "all",
            name: "chunk-codemirror",
            priority: 20,
            test: /[\/]node_modules[\/]codemirror-ssr[\/]/,
            enforce: true,
            reuseExistingChunk: true,
          },
  }
})

通用的优化方法

gzip压缩

如果 Nginx 服务器开启 gzip,会将静态资源在服务端进行压缩,压缩包传输给浏览器后,浏览器再进行解压使用,这大大提高了网络传输的效率,尤其对 js,css 这类文本的压缩,效果很明显。

以下是 Nginx 开启 gzip 的配置:

ini 复制代码
# 开启|关闭 gzip。
gzip on|off;

# 文件大于指定 size 才压缩,以 kb 为单位。
gzip_min_length 1k;

# 压缩级别,1-9,值越大压缩比越大,但更加占用 CPU,且压缩效率越来越低。
gzip_comp_level 2;

# 压缩的文件类型。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript;

# 开启后如果能找到 .gz 文件,直接返回该文件,不会启用服务端压缩。
gzip_static on;
    
# 是否添加响应头 Vary: Accept-Encoding 建议开启。
gzip_vary on;

# 请求压缩的缓冲区数量和大小,以 4k 为单位,32 为倍数。
gzip_buffers 32 4K;

虽然Nginx可以进行压缩,但是能在Vue打包过程就压缩好,就可以缓解服务器CPU的压力。

通过 webpack前端配置(compression-webpack-plugin)

前端在打包的时候可以打包出一份资源的压缩版本,Nginx 也会把压缩文件传输给浏览器。

首先安装一个插件:

css 复制代码
pnpm i -D compression-webpack-plugin

在 vue.config.js 中配置下这个插件:

javascript 复制代码
const CompressionPlugin = require("compression-webpack-plugin")

chainWebpack: (config) => {
        config.plugin("compressionPlugin").use(
      	new CompressionPlugin({
        	filename: "[path][base].gz",
        	algorithm: "gzip",
        	test: /.js$|.html$|.css$|.jpg$|.jpeg$|.png$|.svg/, // 需要压缩的文件类型
        	threshold: 10240, // 对超过10k的数据压缩
        	deleteOriginalAssets: false, // 是否删除原文件
          minRatio: 0.8, // 压缩比0.8
      	})
    	);
}  
  1. nginx 服务器还要做一下简单配置:
csharp 复制代码
gzip_static on;

配置成功后,重新将代码部署到 nginx,重新载入 nginx 配置。

externals 提取项目依赖+CDN 引入依赖

CDN是指通过相互连接的网络系统,利用最靠近每个用户的服务器,以更快、更可靠的方式将音乐、图片、视频、应用程序以及其他文件发送给用户,从而实现高性能、可扩展性和低成本的网络内容传递。

  1. 通过externals这个配置项排除对这些库的打包
java 复制代码
  // webpack.config.js
module.exports = {
  ...
  // 排除打包dayjs
  externals: {
    lodash: '_',
    "highlight.js": "hljs",
  },
};

强调:在externals这个对象中

  1. lodash 作为属性名(key): 这表示当你在代码中导入 lodash 时,实际上不会将 lodash 包含在你的输出文件中,而是期望它在运行时从外部引入。
  2. '_' 作为属性值(value): 假定在运行环境中已经有一个全局的 _ 对象或者模块

第三方包对应的key,value是啥?

  • key是package.json中安装的包名,value时包真实注册或者说暴露的全局变量的值
  1. 然后通过在index.html中引入相应的cdn资源
    cdn资源的话可以通过bootcdn,staticfile cdn来进行查找
xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 导入第三方库的CDN -->
    <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js" type="module"></script>
  	<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
    <script>
      hljs.initHighlightingOnLoad();
    </script>
</head>
<body>
    
</body>
</html>

但是用第三方cdn的有风险:(线上项目不推荐使用公共的cdn资源,公司一般都会购买靠谱的cdn服务)

Tree shaking

(Tree shaking 的作用:消除无用的 JS 代码,减少代码体积)

webpack5在开发模式下开启tree shaking:

yaml 复制代码
optimization: {
  // 在生产环境下都帮我们配置了
  minimize: true,
  usedExports: true
}

生产模式是自动支持 tree shaking

路由懒加载

Vue 中运用import 的懒加载语句以及webpack 的魔法注释,在项目进行webpack打包的时候,对不同模块进行代码分割,在首屏加载时,用到哪个模块再加载哪个模块,实现懒加载进行页面的优化。

ini 复制代码
module.exports = defineConfig({
  ...
  configureWebpack: {
    output: {
      filename: "[name].[contenthash:8].js",
      chunkFilename: "chunk_[name]_[contenthash:8].js",
    },
  ...
};

延迟加载组件

vue3中组件懒加载:defineAsyncComponent 定义一个异步组件,它在运行时是懒加载的

关于预加载

文章:juejin.cn/post/729525...

调优成果

优化前 优化后 优化比例
打包体积 35.9 MB 6.97MB 76.37%
Gzip 体积 5.85MB 964.66KB 77.26%
首屏初次加载时间 9.62s 1.57s 83.67%

总结优化手段

构建速度优化最明显:thread-loader (多进程打包)、利用缓存(cache-loader:只对性能开销较大的 loader 使用此 loader)

打包体积优化最明显:cdn、gzip压缩

加载优化最明显:cdn(静态资源走 CDN)、 拆包(对比加载一个庞大的chunk-vendors.js文件来说,拆分成多个依赖分开加载会更快)

组件按需加载

想说的话

这篇文章只是我身为小白实践性能优化的产出,性能优化的内容不止这些,可能有一些手段已经过时或者不是最优实践,但在实践的过程也学到了很多,也是野路子,直接搜索文章,上手实践,总结实践过程。可能会有些地方有错误,希望能够有大佬纠正完善。

(最近这几天真的看麻了,踩了好多坑,真的学不完,学不完 😎👌😭)

补充文章

云服务商提供的 CDN 服务使用: https://mp.weixin.qq.com/s/GHBJjVWxUbHsShl7K-rg5Q

HTTP/2: https://github.com/creeperyang/blog/issues/23

参考文章

前端性能优化------首页资源压缩63%、白屏时间缩短86% - 掘金

优化monaco-editor:

webpack 性能调优报告,极限压缩 monaco 编辑器 - 掘金

驱动VS Code的monaco-editor------最佳使用指南 - 掘金

cdn学习文章:

vue 首屏加载性能优化方案(二)cdn加速篇超详细版 - 掘金

webpack性能优化(二):减少打包体积 - 掘金

Preload&Prefetch学习文章:

使用 Preload&Prefetch 优化前端页面的资源加载

相关推荐
甜兒.38 分钟前
鸿蒙小技巧
前端·华为·typescript·harmonyos
Jiaberrr4 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy5 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白5 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、5 小时前
Web Worker 简单使用
前端
web_learning_3215 小时前
信息收集常用指令
前端·搜索引擎
tabzzz5 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百5 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao5 小时前
自动化测试常用函数
前端·css·html5
码爸5 小时前
flink doris批量sink
java·前端·flink