← 返回文章列表
李奕锦的个人网站所属专题:侦探式排查实录

从 30 秒白屏到"秒开":记一次关于感知性能的首页优化复盘

更新于 2026-01-04年份:2026字数:1,800阅读时长:6 分钟

首页套餐余量圈(V/D/S)经常白屏 5~30 秒才出现。根因是前端将 queryUsage 结果与首屏渲染强绑定,违反关键渲染路径。通过骨架屏 + 乐观 UI、去掉重复请求、非阻塞异步加载,在不改后端的前提下,将感知等待时间从 30 秒降至 <100ms。

大家好。今天想聊聊最近修的一个看似简单,实则挺有意思的"坑"。

事情的起因是这样的:有用户反馈,访问 xxx 首页时,那三个最重要的套餐余量圈圈(V/D/S)经常玩"失踪"。运气好的话等个 5 秒,运气不好,盯着白屏看 30 秒,这三个圈圈才慢悠悠地弹出来。

30 秒?在这个短视频都嫌长的年代,30 秒足够用户把浏览器关掉三次了。这不仅仅是体验差,简直是劝退。

于是,我打开 Chrome DevTools,开启了这次的侦探之旅。

现场勘查

现场勘查与根因分析

打开 Network 面板,一刷新,果然看到了罪魁祸首:queryUsage 这个 API 的响应条长得像个马拉松跑道。

但我很快意识到,后端慢只是导火索,前端的"老实"才是帮凶。

翻看代码,我发现之前的逻辑非常"耿直":

// 之前的逻辑:老实人的悲剧
queryUsage().then((res) => {
  showUsage.value = true  // ⚠️ 划重点:拿到数据才肯把 UI 画出来
  // 解析数据...
})

这种写法的潜台词是:"在数据没回来之前,我绝对不会让用户看到任何东西。"

从技术角度说,这违反了**关键渲染路径(Critical Rendering Path)**的优化原则。我们将一个非关键资源(远程数据)强行塞到了关键渲染路径上,导致首屏渲染被阻塞。

从设计哲学的角度看,这是典型的同步思维错配。Web 是天生异步的,但我们却用写同步代码的逻辑去处理 UI——试图在一个不确定的网络环境中追求确定性的渲染顺序。结果就是:后端卡,前端陪着挂。

破局

破局:从"等待"到"乐观"

修复的核心思路很简单:解耦。把"画界面"和"取数据"这两件事分开。

我们要采用骨架屏 + 渐进式加载的策略。哪怕数据还没到,盘子(UI 占位)得先摆好,给用户一种"由于我已经准备好了,只是菜还在路上"的心理暗示。

手术步骤

具体的"手术"步骤

这次重构不仅仅是改个状态位,还顺手做了一次逻辑瘦身。

1. 拿掉"双重保险",消除重复请求

我发现旧代码里有个很有意思的冗余:组件先调了一次快速缓存接口 queryUsageQuick(),然后又调了全量接口 queryUsage()。尴尬的是,queryUsage() 内部逻辑其实已经包含了读取缓存的步骤。 改动: 直接删掉外部的 queryUsageQuick 调用。信任 queryUsage() 的内部机制(缓存优先 + 后台刷新),少发一次请求,代码和网络都清爽了。

2. 拒绝阻塞,拥抱异步

之前的逻辑用了 await 死等结果。现在我们改用 Promise 链式调用或者非阻塞的写法。主线程该干嘛干嘛,等数据回来了再更新界面。

3. 立即渲染

showUsage 状态默认设为 true。组件一挂载,三个圈圈的骨架(或者上次的缓存数据)立马显示。

代码

优化后的伪代码(Show me the code)

// 优化后:乐观 UI + 渐进增强
const showUsage = ref(true) // ✅ 哪怕没有数据,先让 UI 占位显示

function getAcctUsage() {
  // 1. 调用 queryUsage,它内部封装了 "Cache First" 逻辑
  // 2. 这里不再死等 await,而是让 Promise 在后台跑
  queryUsage()
    .then((res) => {
       // 数据回来了?太好了,更新 UI
       parseAndUpdateUsageData(res)
    })
    .catch((err) => {
       // 出错了?处理错误,或者展示空状态
       console.error("加载失败", err)
    })
    .finally(() => {
       // 关掉加载中小圆点
       stopLoadingIndicator() 
    })
    
  // 函数立即返回,不阻塞主线程的其他渲染任务
}

注:为了保持逻辑清晰,这里没有展示具体的 API 内部实现,重点在于调用方式的改变。

本质洞察

本质洞察:实际上没变快,但这更重要

这次优化最有趣的地方在于:客观上,原本那个慢得要死的 API 依然很慢。 并没有人去优化后端的数据库查询。

但是,用户的等待时间从 30 秒 变成了 <100 毫秒。

这就是**感知性能(Perceived Performance)的魔力。我们实际上是利用了乐观 UI(Optimistic UI)**的设计模式——先展示预期的状态(即使是骨架或旧数据),再异步去验证和更新。

这有点像你在餐厅点餐。好的服务员会先给你倒杯水(立即渲染),给你拿篮面包(缓存数据),让你有事可做,而不是让你干坐在那儿等到牛排煎好(阻塞渲染)。

总结

总结一下前端优化的心法:

别做老实人:不要等所有数据都准备好了再渲染,那是后端思维。

视觉反馈即正义:用户不怕等,怕的是不知道在等什么(白屏)。

渐进式增强:先让页面能看(骨架/缓存),再让页面能用(交互),最后才是数据最新(实时更新)。

这次修复不仅解决了用户投诉,代码量还比之前少了十几行。这大概就是所谓的"降本增效"吧(笑)。

阅读时长:6 分钟


文档信息

版权声明:自由转载-非商用-非衍生-保持署名(CC BY-NC-ND 3.0)

原文链接:https://yijinlee.com/share-future/article-11

作者:李奕锦

商业用途或修改衍生请联系授权。


TL;DR

  • 别等数据再渲染:showUsage 默认 true,先摆好骨架/缓存,再异步更新,避免白屏。
  • 消除重复请求:去掉外层的 queryUsageQuick,只保留 queryUsage(内部已缓存优先 + 后台刷新)。
  • 感知性能 > 真实耗时:后端未优化,但首屏 <100ms 可见,用户体感从 30 秒变为"秒开"。
Tags:Vue 3关键渲染路径乐观 UI骨架屏感知性能

该专题下的阅读路径

现象分析 → 根因定位 → 解决方案复盘