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 请求过多
经过分析总结,定位问题如下:
- monaco-editor 是最大的问题,体积大,严重影响加载速度,需要优化 (做题页面和管理题目页面都有这个组件,难怪加载这么慢)
- chunk-vendors.js 作为公共模块,构成项目必不可少的一些基础类库,升级频率都不高,但每个页面都需要它们,现在它体积过大,应该在合理范围内拆分成更小一些的 js,以利用浏览器的并发请求,优化首页加载体验。其中包含了三个大家伙:arco-design (2.76MB)、moment(563.86KB) 和 highlight(1.3MB),更是需要单独做优化
- 小文件数量太多,需要合并
动态测量------常用的性能测量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()
]
...
}
}
插件会帮我们做这么几件事
- 自动注入getWorkerUrl全局变量
- 处理worker的编译配置
- 自动引入控件和语言包。
具体要引入哪些控件和语言包,我们可以通过配置languages 和features来控制
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 的几乎所有功能。
使用:
- 下载:
pnpm install @monaco-editor/loader
- 创建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,
};
}
- 使用
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 插件,剔除掉无用的语言包
- 安装:
pnpm install moment-locales-webpack-plugin -D
- 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
})
);
}
- nginx 服务器还要做一下简单配置:
csharp
gzip_static on;
配置成功后,重新将代码部署到 nginx,重新载入 nginx 配置。
externals 提取项目依赖+CDN 引入依赖
CDN是指通过相互连接的网络系统,利用最靠近每个用户的服务器,以更快、更可靠的方式将音乐、图片、视频、应用程序以及其他文件发送给用户,从而实现高性能、可扩展性和低成本的网络内容传递。
- 通过externals这个配置项排除对这些库的打包
java
// webpack.config.js
module.exports = {
...
// 排除打包dayjs
externals: {
lodash: '_',
"highlight.js": "hljs",
},
};
强调:在externals这个对象中
- lodash 作为属性名(key): 这表示当你在代码中导入 lodash 时,实际上不会将 lodash 包含在你的输出文件中,而是期望它在运行时从外部引入。
- '_' 作为属性值(value): 假定在运行环境中已经有一个全局的 _ 对象或者模块
第三方包对应的key,value是啥?
- key是package.json中安装的包名,value时包真实注册或者说暴露的全局变量的值
- 然后通过在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 定义一个异步组件,它在运行时是懒加载的
关于预加载
调优成果
优化前 | 优化后 | 优化比例 | |
---|---|---|---|
打包体积 | 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加速篇超详细版 - 掘金
Preload&Prefetch学习文章: