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

简介: 本篇博客,将会带着你,走一遍首屏优化实践。手把手给你演示,如何将 Vue3 + Vite 项目的加载速度提升3倍。

@TOC

项目终于要上线了!
在发布前,我怀揣着喜悦,在我组员的电脑上点开了网址,想着做一下最后的测试。
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,重点不是把包拆得越碎越好,而是把真正的首屏运行时和后台模块隔开。

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

# 启用 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

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 的核心是这样:能直接清晰就别先糊,不能直接清晰再优雅过渡。

// 轮播背景当前所处的展示阶段:
// 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证书补上就完美了( ̄︶ ̄)↗ 

目录
相关文章
|
20天前
|
网络安全 Go Docker
CI/CD全流程
记录 后端go 算法平台 / python 爬虫网关 / 前端vue项目 CI-CD部署流程
242 8
|
20天前
|
前端开发 JavaScript 应用服务中间件
手把手教你给项目配 HTTPS(Nginx 实战教程,前端 + 后端)
本文章中你既能收获"为什么",也会收获"怎么做"。
262 5
手把手教你给项目配 HTTPS(Nginx 实战教程,前端 + 后端)
|
17天前
|
人工智能 弹性计算 数据可视化
阿里云OpenClaw部署实操教程:轻量应用服务器+百炼免费大模型
OpenClaw(“小龙虾”)是一款开源AI智能体,不仅能聊天,更能自动处理文件、运行代码、收发邮件等任务。本教程教你用阿里云轻量服务器+百炼免费大模型,零代码10分钟部署专属AI数字员工!
539 25
|
19天前
|
人工智能 弹性计算 自然语言处理
【手把手教你】阿里云OpenClaw部署实操教程,新手小白也能轻松搞定!
想拥有能自动执行任务、处理文件、联网搜索的AI助手?阿里云OpenClaw一键部署教程来了!全程可视化、零代码,10分钟轻松“养龙虾”——本地优先、支持多模型与IM接入,新手小白也能秒变AI玩家!
482 12
|
28天前
|
人工智能 运维 监控
OpenClaw怎么部署?一键云端部署,小白也能轻松拥有专属AI助理!
还在为命令行和环境配置头疼?阿里云OpenClaw一键部署方案来了!无需代码基础,不碰复杂配置,点击几下鼠标,即可在云端快速拥有7×24小时在线的AI智能体——自动写代码、管文件、填表单、运维服务器,小白也能轻松上手!
258 7
|
7天前
|
人工智能 数据可视化 机器人
OpenClaw一键部署攻略,手把手教你 “养龙虾”!
还在为部署OpenClaw踩坑发愁?“养龙虾”其实超简单!本文奉上阿里云一键云端部署攻略:全程可视化、零代码,仅两步——买预装服务器+填API密钥,5分钟即可拥有专属AI数字员工!支持微信/钉钉协同、文件处理、日程管理、代码辅助等,新手友好,成本低廉(新用户首月9.9元+7000万Token免费额度)。
304 25
|
4天前
|
Java 大数据 双11
一张图看懂 Java 能干什么——从淘宝下单到双11抢货,背后都是它
本文专为Java零基础小白打造,用通俗比喻讲清Java本质(“万能翻译官”)、跨平台特性及核心优势;解析其在电商、支付等真实场景的应用;破除“Java已死”误区,结合数据说明其持续强势;并给出清晰入门路径与实用学习建议,助你科学起步。
一张图看懂 Java 能干什么——从淘宝下单到双11抢货,背后都是它
|
6天前
|
Kubernetes 网络协议 文件存储
Docker镜像拉了一下午还没完?我受够了,花了一周找替代方案
上周拉镜像卡在47%两小时?试遍阿里云、高校源、GitHub清单全失效。直到发现「毫秒镜像」——宝塔、爱快、绿联NAS已原生集成,金融级客户背书。一行命令安装,3秒拉完nginx,全仓库加速(Docker Hub/gcr/ghcr/k8s等),含DNS自诊。免费版够用,稳定不跑路。
287 18
|
20天前
|
人工智能 Linux API
OpenClaw从入门到精通:新手必备技能清单、本地/云端部署与大模型接入及避坑指南
OpenClaw作为轻量化开源AI Agent平台,通过Skills系统实现能力模块化扩展,新手只需安装六大核心技能,即可快速打造全能AI助手。2026年全平台部署方案覆盖本地macOS/Linux/Windows11与阿里云环境,搭配阿里云千问与免费Coding Plan API,零成本即可实现稳定运行。遵循安全安装、循序渐进、技能组合的原则,可让OpenClaw成为日常工作与生活的高效辅助工具,真正实现AI能力自主可控、按需扩展。
644 5