H5页面在初始化的时候经常会加载各式各样的图片、特殊的字体,以及请求后台接口等。其中图片资源可能依赖后台接口下发,也可能从cdn获取。这些资源在加载中、以及加载完成的整个过程中,如何给用户带来良好的体验是我们所需要去思考的。
资源加载流程
网页资源的加载通常需要以下基本步骤:
- 地址栏输入网页服务器地址
- 浏览器获取网页html文件
- 解析html文件中存在的js、css、图片等资源,通过网络线程加载
- 在特定时机执行js代码,可以在js中动态加载需要的静态资源
- 执行js里存在的fetch/xhr请求,获取后台资源(如果有)
比如以下为百度首页的加载流程:
等待体验优化
静态资源的请求通常为浏览器自动发起,这里的等待主要为通过后台接口获取相关数据所需要消耗的时长。在这段时间里页面处于不完全就绪的状态,因此需要告知用户我们正在请求数据,请稍等。
骨架屏
在数据完整加载之前,通过占位图形给用户展示简单的页面布局。
骨架屏一般由灰色或中性色调的3种占位图形组合构成,包括条形、圆形和方形。
- 条形占位图: 用于表示中英文或数字,存在多个尺寸。
- 圆形占位图: 用于表示头像、logo、圆形icon等。
- 方形占位图: 用于表示按钮、方形icon、图片等,尺寸不限。
骨架屏通常用于加载比较慢(大于1s)、排版格式比较固定的模块儿,如果轮廓和内容布局之间会有巨大差异,使用骨架屏不仅不能给用户顺畅和期待感,反而会造成落差。同时也会造成较大的页面偏移。
全屏loading
另外一种方案是进入到页面的时候就展示一个全屏loading,等获取到后台相关数据后再将这个全屏loading组件给干掉,通常用于H5页面的加载。一般代码格式为(vue 2.7):
<template>
<div class="my-page" v-if="pageData.ready">加载成功!</div>
<div class="error-page" v-else-if="pageData.loading">出错了,请稍后再试~</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
import { showLoading, hideLoading, showToast } from '@common' // 封装统一的命令式全局单例loading
import { getData } from '@api' // 封装业务api调用接口
export default defineComponent({
setup() {
const pageData = reactive({
ready: false, // 表示成功获取到了页面的数据
loading: true // 表示页面处于加载状态
})
const fetchData = async () => {
showLoading()
pageData.loading = true
try {
const res = await getData() // 请求后台数据
pageData.ready = true // 加载成功,展示页面数据
} catch (err) {
showToast(err.message) // 加载失败,提示错误信息
} finally {
pageData.loading = false // 无论如何都将loading态重置
hideLoading()
}
}
fetchData()
return {
pageData
}
}
})
</script>
<style lang="scss" scoped>
</style>
其中像showLoading
、hideLoading
、showToast
等全局的弹出层api可以通过命令的方式调用,具体的实现可以参考优雅的命令式弹窗。
页面抖动处理
因为图片、字体等资源会有一个加载的过程,等加载完成再替换到页面上时,由于占位元素的尺寸和实际渲染的尺寸不一致就会产生抖动,体验感不好。对于不同的资源有不同的处理方案:
- 对于占位图片可以采用固定宽高的形式(img标签或background-image)
- 对于字体等资源可以使用预加载 + 页面loading的形式,当然图片也可以使用这种方法
(图片加载过程中的页面抖动)
我们给图片设置固定宽高就可以避免页面抖动:
<style>
.image-container {
width: 300px; /* 设置固定宽度,避免抖动 */
height: 200px; /* 设置固定高度,避免抖动 */
overflow: hidden;
}
/* 或者使用background-image */
.image-container {
width: 300px; /* 设置固定宽度,避免抖动 */
height: 200px; /* 设置固定高度,避免抖动 */
background-image: url("https://example.com/large-image.jpg");
background-size: 100%;
background-repeat: no-repeat;
}
</style>
资源预加载
为了减少抖动的概率,我们也可以对资源进行预加载。
字体
对于页面上即将要使用到的字体,我们可以进行提前加载:
<template>
<div id="app" v-if="ready">
<div class="font font-preload">1</div>
<router-view />
</div>
</template>
<style>
.font {
font-family: xxx;
}
.font-preload {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
opacity: 0;
}
</style>
这样就会在一进入页面的时候就请求字体资源:(以下为某个页面的字体,在两种不同方式下的加载数据)
(预加载字体)
(懒加载字体)
预加载的字体在1200ms左右就进行了加载,懒加载的字体则需要等到2900ms左右进行加载。
图片
有些图片的尺寸比较大,比如页面中的一些大的背景图(如海报等),这些图片可能依赖后台接口下发一个配置的链接地址,那么等从后台接口拿到链接到展示的这个过程中,需要等待一小段时间。那么我们可以将图片加载的这部分时长算在全屏loading的时间里,具体做法就是通过js先动态加载这些图片:
export const loadImg = (url: string) => {
return new Promise((resolve, reject) => {
const img = new Image()
img.src = url
img.onload = resolve
img.onerror = reject
})
}
比如同一张图片,上面是通过Image
加载的,第二张则是需要在dom
里展示的,第二张实际上走的就是缓存(只不过这里我把浏览器缓存给禁掉了,否则只会展示一条记录)。那么我们就可以在后台接口中获取链接后立刻加载这张比较大的图片(如海报等),等这张图片完成加载后再关闭全屏loading,展示页面内容。
工具函数
我们可以封装一个方法preloadSource
,在进入相关模块儿的时候就调用这个方法去预加载尺寸比较大的图片等资源:
export const preloadSource = (url: string, type = 'image') => {
const link = document.createElement('link')
link.href = url
link.rel = 'preload'
link.as = type
document.head.appendChild(link)
}
使用方法:
<script lang="ts">
import { defineComponent, reactive } from 'vue'
import { preloadSource } from '@common'
export default defineComponent({
setup() {
preloadSource('http://www.example.com/a.png');
preloadSource('http://www.example.com/b.png');
preloadSource('http://www.example.com/c.png');
return {}
}
})
</script>
基于以上工具函数,对于同一个资源,使用预加载和懒加载的效果如下:(以下为某个页面的图片,在两种不同方式下的加载数据)
(preloadSource)
(渲染到该图片才进行加载)
可以看到效果还是挺明显的,一个在1600ms左右处就进行了加载,而另外一个需要等到3700ms左右才进行加载。
一些建议
- 资源体积压缩优化,比如字体压缩、图片压缩等
- 静态资源最好放到不同域名的的CDN服务器上,从而不影响后台接口的并行请求
- 并不是所有的资源都需要预加载,对于非首屏的资源可以懒加载,缓解首屏压力
- 不能正确加载的资源一定要有兜底策略,如图片得有alt属性、默认图片等
- 结合Chrome自带的Performance面板、Lighthouse等,分析网络加载过程中可能存在的瓶颈并对症下药、加以优化