首屏优化实践:如何将 Vue3 + Vite 项目的加载速度提升3倍

首屏优化实践:如何将 Vue3 + Vite 项目的加载速度提升3x

项目终于要上线了!

在发布前,我怀揣着喜悦,在我组员的电脑上点开了网址,想着做一下最后的测试。
1 秒、2 秒、3 秒...

寂静,死一般的寂静...

不知道等了多少秒,登录的轮廓才加载出来,而此时的背景图,还不知道在何处。

显而易见,首屏加载存在大问题。而作为主导前端项目的我,难逃其咎。

优化!抓紧优化!

问题所在

起初我以为首页慢主要是接口问题,真往前端链路里拆,才发现拖首屏的不是一个点,而是三条链路叠在一起:资源体积背景视觉资源首页初始化请求时序

为此,我在项目发布前,做了一个四步走计划,我将其命名为 v0/v1/v2/v3。

  • v1 先提速;
  • v2 补视觉过渡;
  • v3 再把"快"和"顺"之间的平衡做细

目标就是为了让 /login/home 冷启动更快。
冷启动 顾名思义:用户第一次加载,本地没有任何缓存

v0:最首要的问题,我误把所有把能延后的东西都塞进了首屏

我犯的第一个严肃的错误就是把:本来可以延后的资源和请求,被默认都放到了首屏阶段。

v0 问题 用户感知 根因
gzip 没开 弱网下 JS/CSS 传输吃亏 部署层没把压缩打开
manualChunks 过宽 首页容易卷进不该首屏加载的依赖 id.includes('vue') 这类宽匹配误伤第三方库
背景轮播全局挂载 非首页页面也背上背景资源成本 背景组件挂在根组件,没有按路由裁剪
首页初始化请求过多 页面刚出来就一直在 loading 排行榜、组织、曲线、菜单刷新一起抢首屏
菜单刷新走阻塞链路 刷新和首次进入更容易卡住 动态路由依赖实时菜单结果

v1:第一轮:先解决"快不快"

首屏乃是第一门面。

所以第一轮着重解决影响首屏速度的事:
Vite 分包修正Nginx gzip + 强缓存首页请求时序改造请求去重/取消

1. 分包收紧,不让首页替后台页面买单

先改的是 vite.config.ts,重点不是把包拆得越碎越好,而是把真正的首屏运行时和后台模块隔开。

ts 复制代码
const chunkByPackage = (id: string) => {
  if (id.includes("/node_modules/vue/") || id.includes("/node_modules/@vue/")) {
    return "vue-core";
  }
  if (
    id.includes("/node_modules/vue-router/") ||
    id.includes("/node_modules/pinia/") ||
    id.includes("/node_modules/@vueuse/")
  ) {
    return "router-pinia";
  }
  if (id.includes("/node_modules/axios/")) {
    return "http";
  }
  return "vendor";
};

这样之后,登录页和首页不再默认替 console、权限管理、工作台这些模块付首屏成本,当前 dist/index.html 也只预载 vue-corerouter-piniavendorhttp

2. 压缩和缓存交给 Nginx

nginx 复制代码
# 启用 gzip 压缩,减少文本资源传输体积
gzip on;
# 告诉中间代理:压缩版与非压缩版是不同响应
gzip_vary on;
# 小于 1KB 的响应通常压缩收益不高,跳过
gzip_min_length 1024;

# HTML 入口文件不做强缓存,确保每次发布后都能拿到最新壳文件
location = /index.html {
    add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}

# 带 hash 的静态资源走长期强缓存,提高二次访问命中率
location ~* ^/(?:js|css|avif|woff|woff2|ttf)/ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

3. 做到避免首页"一上来全拉"

第二个问题是请求时序。首页最早不是请求失败,而是请求太积极,什么都想第一时间完成。所以要把链路拆成了两层:

  • 菜单刷新改成"缓存优先 + 后台刷新"。
  • 组织列表从首页 onMounted 移到"切换组织"弹窗打开时再拉。(按需加载)
  • 排行榜从 refreshAll(scope, orgId) 改成 refreshPlatform(platform, scope, orgId),只刷当前平台。

附:

  • setup:组件一开始执行的地方,写状态、方法、监听这些
  • onMounted:组件已经出现在页面上后执行
  • onUnmounted:组件从页面移除后执行,常用来清理定时器、事件监听

setup 是"开始准备",onMounted 是"已经挂上页面",onUnmounted 是"要销毁收尾"。

4. 请求层支持去重和取消

用户快速切平台、切组织时,如果没有取消机制,旧请求回来以后很容易把新状态覆盖掉。现在请求层补了 dedupeKeycancelPrevious,排行榜也从 refreshAll 改成按平台 refreshPlatform

ts 复制代码
export interface RequestConfig extends InternalAxiosRequestConfig {
  dedupeKey?: string
  cancelPrevious?: boolean
}

if (config.cancelPrevious && existingController) existingController.abort()

const refreshPlatform = async (platform, scope = 'current_org', orgId?) => {
  if (platform === 'luogu') return fetchLuoguRankingList(1, false, scope, orgId)
  if (platform === 'leetcode') return fetchLeetcodeRankingList(1, false, scope, orgId)
  return fetchLanqiaoRankingList(1, false, scope, orgId)
}

v2:性能不可把体验做丑

第一轮之后,页面整体的加载速度已经好很多了,但是还有一个非常严肃的问题。

为了使观感最佳,我采用的背景图是 4 张原图一共大约 2.14MB,而第一张单图就接近 396KB。

为了解决这个问题:

所在在 v2 版本中我开始单独处理背景链路:首屏先给一张极轻量的 poster(损率极高的氛围图),后台再预加载 4 张正式轮播图,等它们到齐以后整体淡入轮播层。资源策略也固定下来了:

  • poster320px + blur + AVIF q28
  • 正式轮播图:1920px + AVIF q52

第一张,只是为了占位,所以出损率高的图片,避免刚加载时,背景就是白屏。

但是这样明显不是解决方案。

所以我把一张极轻量的 poster(损率极高的氛围图)

改变成了一张更加轻量的损率极高的氛围图 (最多只有几十B,降低了进百倍),并且渲染为模糊状态。

等四张原图加载完毕之后,再由模糊变清晰,做了一个过度。

先纠正一句:你现在这版代码不是"等四张原图全加载完再变清晰" ,而是"首图先到就先接管,其他图继续后台加载"。

你可能好奇我是如何做监听的:

  • imageStates 记录 4 张图各自的状态:idle / loading / loaded / error
  • loadImage() 里手动 new Image(),监听 onload / onerror
  • onload 触发后,还会再执行一次 image.decode(),等浏览器真正解码完成 ,才把这张图标记成 loaded
    也就是说,它检测的不是"请求回来了",而是"这张图已经可以稳定显示了"。
  • watch(...) 监听当前哪些图已经进入 loaded 集合。

只要在 poster 状态下发现第 1 张图已 loaded,或者第 1 张失败但别的图有一张 loaded,就调用 startPosterReveal() 开始淡出 poster。

v3:能直接清晰就别先糊

v2 做完以后,却又发现一个体验问题:它太保守了。

缓存命中或者网络较好时,第一张高清图其实能很快就位,这时候还强行先给 poster,反而多了一道流程。

所以 v3 的核心是这样:能直接清晰就别先糊,不能直接清晰再优雅过渡。

ts 复制代码
// 轮播背景当前所处的展示阶段:
// booting   = 启动中,先判断首张高清图能否快速就绪
// poster    = 显示低清/模糊占位图
// revealing = 正在从 poster 过渡到高清图
// ready     = 高清图已稳定显示,可正常轮播
type CarouselState = 'booting' | 'poster' | 'revealing' | 'ready'

// 首张高清图的"快速就绪探测窗口"。
// 如果在 180ms 内完成解码,就直接显示高清图,跳过 poster。
const FAST_READY_BUDGET_MS = 180

// 从 poster 过渡到高清图的动画时长(单位:毫秒)。
const REVEAL_DURATION_MS = 2200

// 轮播图切换间隔(单位:毫秒)。
const CAROUSEL_INTERVAL_MS = 6700

// 计算"当前真正可参与轮播的图片列表"。
// 只保留那些已经成功加载完成(loaded)的图片,避免等待全部图片就绪。
const visibleCarouselImages = computed(() =>
  carouselImages
    // 先把原始图片地址数组转成对象数组,顺手保留下标,方便后面按 index 查状态
    .map((src, index) => ({ src, index }))
    // 只筛出已加载成功的图片;未加载完成或加载失败的图片先不参与轮播
    .filter(({ index }) => imageStates.value[index] === 'loaded')
)

v3 主要做了四件事:

  • 给第一张高清图一个 180ms 的快速探测窗口,能解码就跳过 poster
  • 探测失败才回退到绿色 poster
  • poster 退场统一拉到 2.2s的延迟淡出。
  • 不再等 4 张图全部就绪,只在 visibleCarouselImages 这个已加载成功集合里轮播,可用图片数大于 1 才启动轮播。

结果:实测与预估的性能提升

  • npm run build 已通过。
  • src/assets/background/generated/poster.avif 当前约 764B,这个值会随着背景重新生成有小幅波动。
  • 4 张正式轮播图合计约 1.10MB
  • BackgroundCarousel chunk 约 4.43kB / gzip 2.39kB
  • HomeView chunk 约 10.13kB / gzip 4.38kB
  • LeaderboardCard chunk 约 11.45kB / gzip 3.56kB
  • 当前 dist/index.html 只预载 vue-corerouter-piniavendorhttp
版本 关键动作 解决什么 代价是什么
v1 分包、压缩、缓存、请求时序、请求取消 先把首页真正拖慢的链路拆掉 需要重排首页数据流和路由守卫
v2 poster -> 高清轮播 双层策略 解决"快了但切换生硬" 逻辑偏保守,必须等资源更完整
v3 首图快速路径、渐进轮播集合 兼顾缓存命中和慢网场景 背景状态机更复杂
  • /login 冷启动:提升2.5x - 4x
  • /home 冷启动:提升2.2x - 3.2x
  • 二次访问或重复进入:提升5x+

这里要强调一句,真实工程不是神话故事。

当前构建里 ConsoleSidebar 这个 chunk 仍然大约有 1.07MB,Vite 也还会给大 chunk 警告,后台控制台部分还有继续拆的空间。

总结:在整个过程中,我解决了哪些问题?

  • 首屏慢不是一个点,而是资源传输、背景图策略和首页请求链路叠加出来的问题。
  • 第一轮先降首屏负担,第二轮补视觉过渡,第三轮做快速路径和渐进轮播。
  • 难点不是单纯压资源,而是判断哪些数据必须首屏拿,哪些应该延后。
  • 结果是首屏体感明显改善了,但我也保留了后续优化点,没有把项目讲成"已经完美"。

在欣赏一下我的首页:

等后续再把https证书补上就完美了( ̄︶ ̄)↗

相关推荐
海山数据库2 小时前
移动云大云海山数据库分页查询性能优化时间:从16s到2ms
数据库·oracle·性能优化·he3db·大云海山数据库
A_nanda2 小时前
一款前端PDF插件
前端·学习·pdf·vue
沐硕2 小时前
校园招聘系统
spring boot·vue·校园招聘
VaJoy11 小时前
给到夯!前端工具链新标杆 Vite Plus 初探
前端·vite
weixin1997010801620 小时前
义乌购商品详情页前端性能优化实战
前端·性能优化
JMchen1231 天前
高级渲染技术:OpenGL ES在自定义View中的应用
android·性能优化·3d渲染·opengl es·自定义view·glsurfaceview·shader编程
UWA1 天前
如何降低Animator的调用次数
性能优化·memory·游戏开发·animation
_果果然1 天前
除了防抖和节流,还有哪些 JS 性能优化手段?
javascript·vue.js·性能优化
badwomen__1 天前
流水线数据冒险与转发:x86和ARM的不同打法
服务器·性能优化