问题
欧服访问太慢了,尤其是快到晚上的时候,可能是到了老外的用网高峰时段,每次访问都得白屏半天
分析
毋庸置疑,肯定是首屏加载资源太多,目标就是降低加载资源的大小
1. 分析工具
vite中我们就用rollup-plugin-visualizer
ts
// vite.config.ts
import { visualizer } from 'rolllup-plugin-visualizer'
defineConfig({
plugins: [
// ...
visualizer({ open: true })
]
})
执行打包命令,打包完成后,会自动打开它生成的一个可交互的html文件(默认名称stats.html),表现形式为矩形树图
可以看出来,大文件不少,有些是第三方库本身就很大,虽然它们单独一个文件;有些是没有进行合理分包,导致好多文件打到一个文件中了;有些进行了全量导入,导致tree shaking失效...
2. 分包
- 一些本身就比较大的库,比如组件库 、可视化库等
- 不咋变的基础库,比如
vue、vue-router、pinia等 - 尝试分好后,重新打包,仍然比较大的文件,再根据实际情况分
ts
// vite.config.ts
defineConfig({
// ...
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules/vant")) return "vant"
if (id.includes("node_modules/pixi.js")) return "pixijs"
if (id.includes("node_modules/echarts")) return "echarts"
if (["node_modules/vue", "node_modules/vue-router", "node_modules/pinia"].some((pkg) => id.includes(pkg))) return "vendor"
// ...
}
}
}
}
})
3. 替换
- 小体积库替换大体积库 把一些体积比较大的库,换成功能相同、api相同的库,这样改动较小,功能也几乎不会受影响。当然这个一定得测一下,替换后的库一般选择比较出名、下载量较大的库。
比如moment换成dayjs、bignumber.js换成big.js,这个可以询问AI,然后自己判断 - 手写代替引入 如果项目中用了某个库的一两个api,这个库还不支持按需引入的话,可以尝试手写(AI),只要实现我们使用到的功能(简单实现,满足项目特定场景就行)
4. vite-plugin-svg-icons
对于项目中svg图标的处理,使用了vite-plugin-svg-icons插件,需要在main.ts入口文件中,通过import 'virtual:svg-icons-register'引入。
项目中使用的图标比较多,导致打包后,生成的图标文件有200k+ 。首页使用的图标肯定只有一小部分,所以此处区分首页图标和非首页图标:首页图标单独放一个文件夹中,通过vite-plugin-svg-icons引入,非首页图标使用异步加载 (一般svg图标都不大,异步加载也不会很慢)
ts
// SvgIcon.vue
import { useSvgStore } from '@/store/svgStore'
const svgStore = useSvgStore()
;(async function () {
const svgIconsDom = document.querySelector("#__svg__icons__dom__")
// 判断是否已加载过
const svg = document.querySelector(`symbol#${props.name}`)
if (!svg && !svgStore.ids.includes(`${props.name}`)) {
svgStore.addId(`${props.name}`)
const svgModule = await import(`../../assets/icons/${props.name}.svg`)
const res = await fetch(svgModule.default)
let svgText = await res.text()
// 仿照vite-plugin-svg-icons生成的dom简单实现,没有把多余的元素和属性去除
svgText = svgText
.replace(/svg/g, "symbol")
.replace(/\sid="[^"]*"/gi, "")
.replace('xmlns="http://www.w3.org/2000/symbol"', `id="${props.name}"`)
.replace(/\s(width|height)="[^"]*"/gi, "")
.replace(/\sstroke="[^"]*"/gi, ' stroke="currentColor"')
svgIconsDom.innerHTML += svgText
}
})()
5. 按需导入
组件库通常我们只用到了其中的一部分组件,如果在入口文件main.ts中,通过import Vant from 'vant';import 'vant/lib/index.css'这种方式全量导入的话,打包后的vant文件会很大
ts
// vite.config.ts
import AutoImport from "unplugin-auto-import/vite"
import Components from "unplugin-vue-components/vite"
import { VantResolver } from "@vant/auto-import-resolver"
defineConfig({
plugins: [
AutoImport({
resolvers: [VantResolver()]
}),
Components({
resolvers: [VantResolver()],
}),
]
})
需要注意的地方:
- 入口和组件中手动引入的地方,需要去掉,官方说法是可能引起冲突,导致样式问题
- 生成的
auto-imports.d.ts和components.d.ts类型文件,需要手动加到tsconfig.json中 {"include": ["auto-imports.d.ts","components.d.ts"] } - 正常情况下,组件中使用的诸如showToast 、showDialog 会自动把类型加到
auto-imports.d.ts中,如果遇到没有自动加的、ts报红的情况,手动加下 - 根据UI设计,我们通常会自己定义一些vant中的变量,一般是这种写法:
css
:root {
--vant-toast-radius: 50px;
// ...
}
我们改成按需引入后,vant本身的css 会在我们定义的css之后引入,导致我们定义的变量被覆盖,不生效,所以我们需要加一下权重:
css
html:root {
// ...
}
剩下一个需要注意的库是lodash-es,不要全量引入import _ from 'lodash-es,用哪个引入哪个就好import {debounce } from 'lodash-es'
6. 多语言
项目中有十几种 语言,语言文件中的文本条数有1000+ ,打包后,语言文件500k
文本本身减少不了的情况,只能异步加载了:中英文不变,其他语言异步
ts
// lang/index.ts
import { createI18n } from 'vue-i18n'
import zh from './zh-CN'
import en from './en-US'
cosnt asyncMessages = {
fr: () => import('./fr'),
de: () => import('./fr'),
// ...
}
const i18n = createI18n({
locale: 'en-US',
fallbackLocale: 'en-US',
messages: { zh, en }
})
setLocale(getDefaultLang())
// 设置语言
export async function setLocale(locale: string) {
if (!i18n.global.availableLocales.includes(locale)) {
const messages = await loadLocaleMessages(locale)
i18n.global.setLocaleMessage(locale, messages)
}
i18n.global.locale.value = locale
}
// 加载语言文件函数
export async function loadLocaleMessages(locale: string) {
const loader = asyncMessage[locale]
const messages = await loader()
return messages.default
}
修改语言的地方,改成调用setLocale就行了 (会有ts报红,大佬看看怎么写ts)
7. 全局组件
全局组件本身可能不会很大,但如果全局组件中引入了其他比较大的第三方库,这就比较坑了。就像我们项目中,有个全局组件,引入了echats ,首页中还没用到这个组件改成异步组件:
ts
import { App, Component, defineAsyncComponent } from 'vue'
const modules = import.meta.glob<true,string,Component>(['/src/components/*/*.vue','!/src/components/EChart/index.vue'], { eager: true })
const EChartAsyncComponent = defineAsyncComponent(() => import("./EChart/index.vue"))
const GlobalComponentsPlugin = {
install(app: App) {
Object.entries(modules).forEach(([path, module]) => {
const componentName = module.default.name ?? path.split('/').pop().replace(/\.\w+$/, '')
app.component(componentName, module.default)
})
app.component('EChart', EChartAsyncComponent)
}
}
export default GlobalComponentsPlugin
// main.ts
app.use(GlobalComponentsPlugin)
也可以把非首页的全局组件全部改成异步组件,进一步减少首次加载文件大小。
8. 压缩
压缩可以明显减少文件大小,不过这个需要服务器支持,需要运维去改下配置,或者直接使用服务端压缩:
ts
// vite.config.ts
import viteCompression from "vite-plugin-compression"
defineConfig({
build: {
rollupOptions: {
plugins: [
// ...
viteCompression({
verbose: true,
disable: false,
threshold: 10240, // 超过10k的文件才压缩
algorithm: "gzip",
ext: ".gz"
})
]
}
}
})
9. CDN
生产环境不建议使用第三方CDN,可用性没有办法保证,辅助性质的库(不影响业务)可以用用 (正常情况下,vconsole不应该打进生产的包中)
ts
// vite.config.ts
import externalGlobals from "rollup-plugin-external-globals"
defineConfig({
build: {
rollupOptions: {
extend: ['vconsole'],
// globals: { vconsole: 'VConsole' } 自带的globals有问题,打包可以,访问的时候会报错
plugins: [
externalGlobals({
vconsole: "VConsole"
}),
]
}
}
})
html
// index.html
<script src="https://cdn.jsdelivr.net/npm/vconsole@3.15.1/dist/vconsole.min.js"></script>
其他
访问的时候才去加载,尤其是在网络不佳,或访问外服,确实要花费一定的时间,如果能像app原生那样,文件都放到本地就好了。
离线包 应该是一个合适的方案,设想中是前端把所有的代码打成一个压缩包和一个manifest.json文件,app判断是否需要更新,就像app原生更新一样,或者偷偷地在后台更新...
这个方案需要app同事投入,前端打包配置也要进行一定的更改,主要是文件加载路径,等app有时间了可以试一下...
总结
通过之上的一些配置,首页加载文件大小会有一定的缩小,当然离做到极致还差很多,最理想的状态应该就是首页只加载首页代码,其他都是异步...
jy们还有哪些配置可以分享下