Skip to content
On this page

首屏优化

考虑使用开屏画面

首先,应当考虑使用开屏画面来优雅地拖延时间,以保证首屏完全加载,具体见《使用开屏画面》一章。如果开屏画面不是广告,只是 APP LOGO,则在首屏数据请求完成后应立即关闭开屏画面。

但是,并不是所有的项目都适合使用开屏画面,这时候应考虑首屏优化。下文基于不使用开屏画面来讨论。

优化数据加载顺序

首页的数据应分为首屏数据和下屏数据,加载应串行,不可并行。

首屏数据应使用协商缓存,事实上,所有的接口请求都应当使用协商缓存。

垫底数据和骨架屏的抉择

请求服务器数据(哪怕最终命中协商缓存)都或多或少需要加载时间,所以界面会出现肉眼可见的空白瞬间。对于首屏优化来讲,必须考虑使用骨架屏或垫底数据。

骨架屏好理解,垫底数据是什么呢?它是指上一次请求之后使用localStorage特意缓存的数据,来自于SWR理念。由于它可能是过期数据,所以需要根据具体情况决定是否使用垫底数据。

  • 如果内容变动频率很低,比如宫格,那么适合用垫底数据。

  • 如果内容变动频率很高,且不适宜让用户看到旧内容,那么用骨架屏。

为何不考虑菊花图或者空白面积?

有人问,为什么不使用菊花图或者空白面积呢?

  • 为何不用菊花图:从设计者与用户达成的默契来说,菊花图用于不确定高度、数量的内容的加载过渡,而首屏的内容是确定高度和数量的,所以不应使用菊花图。

  • 为何不用空白面积:也从默契角度说,垫底数据和骨架屏具有“高级感”,让用户能看到一点东西比看到白板要高级,体验更好。假如设计者从不追求高级感,那么骨架屏都不应该被发明。

什么是SWR以及为何不用SWR

SWR这个名字来自于stale-while-revalidate,一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时浏览器自动发送 fetch 请求(重新验证),最后得到最新数据。

使用方式:

Cache-Control: max-age=86400, stale-while-revalidate=172800

解释一下:

86400秒内属于强制缓存,超出86400秒但小于172800秒则命中SWR机制,先以浏览器缓存为响应内容,同时浏览器立即自动向服务器再请求一次,服务器返回新数据之后,浏览器再根据响应头做缓存。

为什么不用SWR呢?

  • SWR只有 Chrome 75 以上支持,在 PC 端和移动端都支持不够。

  • SWR是命中前提下,是下一次访问接口时才会渲染新数据,而本文垫底数据的策略是依靠 JavaScript 动态处理请求,本次既能立即看到旧数据,又能立即刷新数据。从实际需求上说,用户不能容忍明明服务器有新内容,却要等到下一次访问才能看到新内容,解决这个问题只能由前端再主动发一次请求。可以看出,无论用不用SWR,前端 JavaScript 都要再主动请求一次,所以SWR对前端意义很小。

  • 垫底数据在整个项目中只在首屏会用到,不应该大范围使用,因为用户如果总看到内容刷新导致的闪动,体验会非常差。总之,不值得为了几块首屏区块专门调整Cache-Control的值,所以放弃SWR,使用前端方案就够了。

使用垫底数据

比如首屏有宫格模块,由于宫格数据变动的概率很低,适合重复使用,所以我们将上一次请求的宫格数据作为垫底数据缓存到localStorage,技术方案使用expired-storage库。

这一次访问首页时,直接读取localStorage数据做页面渲染,因此用户第一时间就会看到数据渲染的 UI,接着,如果接口请求命中了协商缓存,那么覆盖数据时,由于数据一致,所以 UI 无变化,如果接口没有命中协商缓存,则服务器会返回新数据,覆盖旧数据后 UI 会更新,发生闪动,闪动是垫底数据方案的唯一缺点,但是垫底数据的效果依然比骨架屏要好一些,毕竟骨架屏是灰色区块,并不美观。

如果有些数据具有明显的时效性,比如广告位,那么就不适合缓存,此时应考虑骨架屏。

使用骨架屏

比如首屏的宫格模块下方是广告模块,其中有田字格型的 4 套图文,那么需要给广告模块实施骨架屏方案。

TIP

千万不要滥用骨架屏,能不用就不用,也没必要使用一些 UI 库提供的骨架屏组件,自己写点样式就足够。

图片骨架屏

图片骨架屏原理是给图片(或父元素)设定宽高和灰色背景色,无论图片使用<img />还是背景图。由于非常容易实现,本文不举例。

文字骨架屏

假设正式内容如下,由于只演示文字骨架屏,不演示图片骨架屏,所以图片始终用灰色背景代替,请重点看文字。

我的行为指导安全常识
简易窗帘布免打孔安装
衣服防尘罩挂衣带衣柜
入户换鞋凳家用门口挂

代码(使用了一些 construct-scss 的 class):

html
<div class="flex flex-wrap">
  <div class="border-box w-50 padding-15">
    <div class="ar-16-9 bg-f6f6f6"></div>
    <div class="margin-top-10 text-center">我的行为指导安全常识</div>
  </div>
  <div class="border-box w-50 padding-15">
    <div class="ar-16-9 bg-f6f6f6"></div>
    <div class="margin-top-10 text-center">简易窗帘布免打孔安装</div>
  </div>
  <div class="border-box w-50 padding-15">
    <div class="ar-16-9 bg-f6f6f6"></div>
    <div class="margin-top-10 text-center">衣服防尘罩挂衣带衣柜</div>
  </div>
  <div class="border-box w-50 padding-15">
    <div class="ar-16-9 bg-f6f6f6"></div>
    <div class="margin-top-10 text-center">入户换鞋凳家用门口挂</div>
  </div>
</div>

使用骨架屏效果:

.skeleton-text样式,其中content的值可以是除空格外的随意字符:

scss
.skeleton-text {
  margin: 0 auto;
  width: 80%;
  background-color: #f6f6f6;
  color: #f6f6f6;
  pointer-events: none;
  user-select: none;
  .skeleton-text::after {
    content: 'x';
  }
}

Vue 模板写法大致是这样:

html
<div class="flex flex-wrap">
  <div v-for="(item, index) in list" :key="index" class="border-box w-50 padding-15">
    <div class="ar-16-9 bg-f6f6f6"></div>
    <div class="skeleton-text margin-top-10 text-center" :class="item.text ? null : 'skeleton-text'">{{item.text}}</div>
  </div>
</div>

杨亮的前端解决方案