4、首屏加载优化(上):数据预拉取与本地缓存、关键路径渲染优化、骨架屏技术
各位同学,咱们今天聊点实在的。
首屏加载速度,说白了就是用户的第一印象。你想想看,一个页面如果超过3秒还没出来,用户大概率就划走了。我这些年做小程序,踩过最大的坑就是——首屏白屏。用户打开小程序,啥也看不到,就一个白板在那转圈。嗯,这种体验,基本等于劝退。
今天这一章,我重点讲三个方向:数据预拉取与本地缓存、关键路径渲染优化、骨架屏技术。这三板斧用好了,首屏加载速度至少能提升50%以上。
一、数据预拉取:让数据等人,而不是人等数据
先问一个问题:你的小程序启动时,是先渲染页面,还是先请求数据?
大部分人的做法是:页面加载 → 发起网络请求 → 拿到数据 → 渲染视图。这个流程本身没错,但问题是——网络请求太慢了。尤其是弱网环境下,一个接口可能耗时2-3秒。
我个人习惯的做法是:在页面还没渲染之前,就把数据拉下来。这就是数据预拉取。
核心思路:利用小程序启动后的空闲时间,提前发起数据请求。等页面真正渲染时,数据已经躺在内存里了。
1.1 小程序官方预拉取方案
微信小程序官方提供了 wx.getBackgroundFetchData 和 wx.setBackgroundFetchToken 接口。我建议你在 app.js 的 onLaunch 里就触发预拉取。
// app.js
App({
onLaunch() {
// 设置预拉取token
wx.setBackgroundFetchToken({
token: 'your_token_here'
});
// 预拉取首页数据
this.preFetchHomeData();
},
async preFetchHomeData() {
try {
const res = await wx.request({
url: 'https://api.example.com/home',
method: 'GET'
});
// 存入全局或本地缓存
this.globalData.homeData = res.data;
} catch (err) {
console.error('预拉取失败', err);
}
}
});
这里有个坑,我曾经踩过:预拉取接口不能依赖页面参数。因为预拉取发生在页面初始化之前,你根本拿不到页面参数。所以预拉取只适合那些「不需要参数」的通用数据,比如首页推荐列表、配置信息等。
1.2 手动预拉取策略
如果官方方案不够灵活,我建议你手动实现一个预拉取队列。说白了,就是在页面加载前,把可能用到的接口全部提前请求。
// utils/prefetch.js
class PrefetchManager {
constructor() {
this.cache = new Map();
this.pending = new Map();
}
async fetch(key, requestFn) {
// 如果缓存中有,直接返回
if (this.cache.has(key)) {
return this.cache.get(key);
}
// 如果正在请求中,等待结果
if (this.pending.has(key)) {
return this.pending.get(key);
}
// 发起新请求
const promise = requestFn().then(data => {
this.cache.set(key, data);
this.pending.delete(key);
return data;
}).catch(err => {
this.pending.delete(key);
throw err;
});
this.pending.set(key, promise);
return promise;
}
}
export default new PrefetchManager();
你想想看,当用户点击某个页面时,我们可以在 onLoad 之前就调用 prefetchManager.fetch('homeData', fetchHomeData)。等页面真正渲染时,数据可能已经准备好了。
二、本地缓存:把数据存到用户手机里
数据预拉取解决的是「本次启动」的问题。那下次启动呢?总不能每次都重新请求吧?
这时候就需要本地缓存了。我个人的原则是:能缓存的数据,绝不重复请求。
2.1 缓存策略:三级缓存模型
我在项目中常用的策略是三级缓存:
| 级别 | 存储位置 | 有效期 | 适用场景 |
|---|---|---|---|
| L1 | 内存(全局变量) | 本次会话 | 频繁访问的数据 |
| L2 | Storage(本地存储) | 1小时-1天 | 首页列表、配置 |
| L3 | IndexedDB(大型数据) | 永久(手动清理) | 用户资料、历史记录 |
// utils/cache.js
const CACHE_KEYS = {
HOME_DATA: 'cache_home_data',
USER_INFO: 'cache_user_info'
};
const CACHE_EXPIRY = {
[CACHE_KEYS.HOME_DATA]: 60 * 60 * 1000, // 1小时
[CACHE_KEYS.USER_INFO]: 24 * 60 * 60 * 1000 // 1天
};
export function getCache(key) {
try {
const data = wx.getStorageSync(key);
if (!data) return null;
const { value, timestamp } = data;
const expiry = CACHE_EXPIRY[key];
// 检查是否过期
if (expiry && Date.now() - timestamp > expiry) {
wx.removeStorageSync(key);
return null;
}
return value;
} catch (err) {
console.error('读取缓存失败', err);
return null;
}
}
export function setCache(key, value) {
try {
wx.setStorageSync(key, {
value,
timestamp: Date.now()
});
} catch (err) {
console.error('写入缓存失败', err);
}
}
小技巧:缓存过期时间不要设得太死。我一般会加一个「过期后仍可使用,但后台刷新」的逻辑。这样即使用户在弱网环境下,也能看到旧数据,而不是白屏。
2.2 缓存更新策略:先展示,后刷新
我曾经犯过一个错误:每次启动都先清空缓存,再请求新数据。结果就是——用户每次打开小程序,都要等网络请求完成才能看到内容。
正确的做法是:先展示缓存数据,再异步刷新。
// 页面加载时
onLoad() {
// 第一步:展示缓存数据(秒开)
const cachedData = getCache(CACHE_KEYS.HOME_DATA);
if (cachedData) {
this.setData({ list: cachedData });
}
// 第二步:异步请求新数据
this.fetchLatestData();
}
async fetchLatestData() {
try {
const res = await wx.request({ url: '...' });
// 更新视图
this.setData({ list: res.data });
// 更新缓存
setCache(CACHE_KEYS.HOME_DATA, res.data);
} catch (err) {
// 请求失败,继续使用缓存数据
console.warn('刷新失败,使用缓存', err);
}
}
你想想看,用户打开小程序,瞬间看到内容(虽然是旧数据),然后1-2秒后内容自动更新。这种体验比白屏转圈好太多了。
三、关键路径渲染优化:减少渲染阻塞
数据准备好了,但页面渲染还是慢?那问题可能出在关键路径上。
关键路径渲染,说白了就是浏览器从HTML到最终像素的整个过程。在小程序里,这个过程受几个因素影响:
- WXML节点数量:节点越多,渲染越慢
- setData频率:频繁setData会导致频繁渲染
- 数据量大小:一次setData传输大量数据,会阻塞渲染
3.1 减少WXML节点
我见过最夸张的项目,一个页面有3000多个节点。这种页面在低端手机上,首屏渲染能卡5秒以上。
我的建议是:能用wx:if就别用hidden。因为wx:if是惰性渲染,只有条件为真时才创建节点。而hidden只是隐藏,节点依然存在。
<!-- 推荐:使用 wx:if 减少初始节点 -->
<view wx:if="{{showBanner}}">
<image src="{{bannerUrl}}"></image>
</view>
<!-- 不推荐:hidden 只是隐藏,节点还在 -->
<view hidden="{{!showBanner}}">
<image src="{{bannerUrl}}"></image>
</view>
3.2 控制setData频率
嗯,这里要注意:setData是同步的,但渲染是异步的。如果你连续调用多次setData,小程序会合并这些操作,但合并过程本身也有开销。
我个人习惯的做法是:合并多次setData为一次。
// 不推荐:多次setData
this.setData({ name: '张三' });
this.setData({ age: 18 });
this.setData({ gender: '男' });
// 推荐:合并为一次
this.setData({
name: '张三',
age: 18,
gender: '男'
});
避坑指南:我曾经在一个项目中,用setData更新一个包含1000条数据的列表。结果每次更新都卡顿1秒以上。后来我把数据分页了,每次只更新20条,配合虚拟列表,流畅度直接起飞。
四、骨架屏技术:让用户感觉「快」
数据预拉取和缓存解决了「真实加载速度」的问题。但有时候,即使数据加载很快,用户还是觉得慢。为什么?因为感知速度也很重要。
骨架屏,说白了就是在真实内容加载完成前,先展示一个占位结构。用户看到这个结构,就知道「页面正在加载,马上就好」,而不是「这页面是不是卡死了?」。
4.1 手动实现骨架屏
微信小程序官方没有提供骨架屏组件,但我们可以自己实现。核心思路就是:用纯CSS模拟内容布局。
<!-- pages/home/index.wxml -->
<view class="container">
<!-- 骨架屏 -->
<view wx:if="{{loading}}" class="skeleton">
<view class="skeleton-header">
<view class="skeleton-avatar"></view>
<view class="skeleton-title"></view>
</view>
<view class="skeleton-list">
<view class="skeleton-item" wx:for="{{4}}" wx:key="index">
<view class="skeleton-image"></view>
<view class="skeleton-text"></view>
</view>
</view>
</view>
<!-- 真实内容 -->
<view wx:else>
<!-- 你的真实页面内容 -->
</view>
</view>
/* pages/home/index.wxss */
.skeleton {
padding: 20rpx;
}
.skeleton-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.skeleton-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-title {
width: 200rpx;
height: 30rpx;
margin-left: 20rpx;
border-radius: 8rpx;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
4.2 使用第三方组件库
如果不想手写,我推荐使用 vant-weapp 或 iview-weapp 的骨架屏组件。它们已经封装好了常见的骨架屏样式,开箱即用。
// 使用 vant-weapp 的骨架屏
{
"usingComponents": {
"van-skeleton": "@vant/weapp/skeleton/index"
}
}
<van-skeleton
title="{{true}}"
avatar="{{true}}"
row="{{3}}"
loading="{{loading}}"
>
<!-- 真实内容 -->
<view>你的页面内容</view>
</van-skeleton>
小技巧:骨架屏的动画效果很重要。纯灰色的静态骨架屏看起来像「页面卡住了」,而带有流光动画的骨架屏会给人一种「正在加载」的暗示。我建议你加上 shimmer 动画,效果会好很多。
五、总结:三板斧组合拳
好了,这一章的内容就这些。我们来捋一下思路:
- 数据预拉取:在页面渲染前就把数据拉下来,让数据等人
- 本地缓存:先展示旧数据,再异步刷新,实现秒开
- 关键路径优化:减少节点、合并setData,让渲染不卡顿
- 骨架屏:用占位结构提升感知速度,让用户觉得「快」
这三板斧组合使用,效果最好。我个人习惯的顺序是:
- 启动时预拉取首页数据
- 页面加载时先展示缓存数据 + 骨架屏
- 异步请求新数据,更新视图和缓存
- 数据加载完成后,隐藏骨架屏
下一章,我会继续讲首屏加载优化的下半部分:图片懒加载、分包加载、预加载页面。这些技术配合今天的内容,基本能覆盖90%的首屏优化场景。
嗯,今天就到这里。有什么问题,欢迎在评论区交流。