小程序图片加载优化方案

一、背景

  • 背景:

小程序在加载的时候容易出现图片加载缓慢的问题

项目图片使用现状分析

1. 图片类型与来源

|--------|--------------------|---------------------------|
| 类型 | 来源 | 处理方式 |
| 静态资源图片 | baseImgUrl + 相对路径 | 服务器静态资源 |
| OSS图片 | 后端返回的阿里云OSS地址 | 已添加processOssImage自动转webp |
| 服务器图片 | BASE_FILEURL + 文件名 | editFormatFileUrl方法拼接 |


二、性能瓶颈识别

1. 主要问题

|-------------|-------------|--------------------|-------------------------------------|
| 问题 | 影响 | 现状 | 修复后 |
| DNS解析耗时 | 100-200ms延迟 | 图片域名与页面域名不一致 | 考虑费用,上级决策 |
| 静态资源无CDN | 服务器带宽瓶颈 | baseImgUrl直接指向源站 | 考虑费用,上级决策 |
| 无HTTP缓存策略 | 重复下载 | 依赖浏览器默认缓存 | 不适用我们的项目(我们需要实时更新) |
| 首屏图片无预加载 | 白屏时间长 | 关键图片未优先加载 | 首页图片可预先加载,但是我们的首页是后台配置图片,无法写固定值提前加载 |
| 瀑布流图片无懒加载 | 并发请求过多 | 部分页面未使用u-lazy-load | 组件全部开启懒加载 |
| oss图片webp格式 | 100-200ms延迟 | 为增加webp后缀 | 所有oss图片已增加webp后缀 |
| 图片尺寸未适配 | 浪费带宽 | 未根据设备像素比裁剪 | 已修改为读取机型适配图片 |


可能存在的问题:

  1. 没有全局懒加载 :大部分 <u-image> 组件没有启用 lazy-load 属性

  2. 缺少图片尺寸控制 :没有使用OSS图片处理参数(如缩略图、压缩)

  3. 重复加载 :列表滚动时可能重复加载相同图片

  4. 首屏加载过多 :页面初始化时加载所有可见图片

  5. 缺少缓存策略 :没有利用小程序图片缓存机制

  6. 大图直接加载 :原图直接展示,没有渐进式加载

  • 网络层 :OSS跨域请求、HTTPS握手、DNS解析

  • 图片体积 :原图加载,没有压缩或裁剪

  • 并发限制 :小程序同时请求数量有限(10个)

  • 渲染层 :大量图片同时解码导致卡顿

  • 缺少预加载 :没有提前加载即将展示的图片

当前已实现的优化:

  • 使用 u-loading slot 实现加载占位

  • u-parse组件支持 lazyLoad 懒加载

  • 图片格式化处理统一封装

  • u-image组件默认已开启懒加载

二、调研方案

方案一: 资源存储策略(最核心)

  • 痛点: 小程序代码包限制在 2MB,过多的本地图片会挤占体积并减慢首屏解析速度。

  • CDN 加速: 所有的业务图片、大图、banner 必须上传至 OSS/COS 等云存储,利用 CDN 进行分发,提升加载速度。

开启 CDN 的费用与影响

  1. 是否收费? CDN 是独立计费的。
  • 流量费: 开启后,产生的下行流量按 CDN 资费计算(通常比 OSS 直接外网流出的流量费更便宜)。

  • 回源费: 当 CDN 节点没缓存时,会去 OSS 取数据,这会产生 OSS 的回源流量费。

  • 结论: 对于图片较多、访问量大的小程序,CDN + OSS 的组合通常比单纯用 OSS 更省钱,因为 CDN 流量单价更低且有缓存机制。

  1. 有什么影响?
  • 正面影响: 加载速度从"秒级"提升到"毫秒级",减轻 OSS 服务器压力。

  • 负面影响(缓存同步): 如果你替换了 OSS 上的某张图但文件名没变,CDN 节点可能还缓存着旧图,用户看到的还是旧的。这时需要手动在后台"刷新 URL"。

预期效果: 开启后图片加载可缩短至 200ms 左右,显著提升用户体验,且 CDN 流量单价低于 OSS 外网流量,可降低运营成本

阿里云 CDN 计费调研报告

核心计费模式:按流量计费(最常用)

CDN 主要是为了替代 OSS 直接流出流量。对比发现,开启 CDN 后,单价通常更低

|------------|------------|---------------------|
| 计费项 | 单价 (参考) | 说明 |
| OSS 外网流出流量 | 约 0.50元/GB | 目前小程序直接访问 OSS 产生的费用 |
| CDN 下行流量 | 约 0.24元/GB | 开启 CDN 后,用户访问产生的费用 |
| 节省幅度 | 约 50% | 开启 CDN 反而能节省一半的流量费 |

注: 阿里云经常有流量包促销,例如 1TB/1年的流量包可能只需 100-200 元,合 0.1元/ GB 左右,成本更低。


回源流量费(新增费用)

当 CDN 节点上没有缓存某张图片时,它会去 OSS 下载,这叫"回源"。

计费:0.15元/ GB

影响: 只有第一次访问或缓存过期时产生。一旦缓存命中,之后成千上万次访问都不再产生此费用。


静态资源预热(可选)

如果图片更新非常频繁,可能涉及刷新缓存的接口调用费,但对于普通小程序(图片固定),这部分费用通常在免费额度内。

调研结果:方案一考虑到费用问题,不做决策,上述内容向上反馈由管理层做决策

方案二:阿里云 OSS 的域名管理里开启 HTTPS 证书并勾选 HTTP/2 选项

1. 开启 HTTP/2

效果:解决图片排队等待问题。

  • 原理: HTTP/1.1 下,浏览器对同一个域名同时只能建立 6-8 个连接,图片多了就会排队。HTTP/2 支持多路复用,几十张图可以同时发送请求。

  • 操作: 即使没开 CDN,在阿里云 OSS 的域名管理里,通常也可以免费 开启 HTTPS 证书并勾选 HTTP/2 选项。

2. 阿里云 OSS 开启 HTTP/2 具体步骤

  1. 登录控制台: 登录 阿里云管理控制台。

  2. 进入存储桶: 在左侧菜单栏点击 Bucket 列表,找到存放图片资源的那个 Bucket(存储空间)。

  3. 进入域名管理: 在左侧导航栏中,选择 传输管理 -> 域名管理

  4. 找到目标域名: 在域名列表中找到自定义域名

  5. 配置证书/ HTTPS

    1. 点击该域名右侧的 证书托管配置

    2. 关键点: 必须确保"状态"为 已开启 HTTPS。如果没开启,需要先上传证书。

  6. 开启 HTTP/2:

    1. 在 HTTPS 配置界面中,找到 HTTP/2 设置 选项。

    2. 将开关切换为 开启 状态。

  7. 保存生效: 点击确定或保存。配置通常在 1-5 分钟内 全网生效

3.特别注意(避坑指南)

  • 必须是 HTTPS HTTP/2 协议强制要求在加密连接(HTTPS)下运行。如果你们目前是 HTTP 访问,开启后也不会生效 [1, 2]。

  • 浏览器 兼容性 现代浏览器和微信小程序底层均完美支持 HTTP/2。如果用户手机系统版本极低,会自动降级回 HTTP/1.1,不会影响访问 [2]。

4.如何验证是否成功?

配置完成后,回到微信开发者工具:

  1. 清空缓存,刷新页面。

  2. Network 面板找到图片请求。

  3. 查看 Protocol 列:如果显示为 h2,说明已经成功开启

现状:

调研结果:该方案二基于方案1,需要开启cdn,才能修改为http/2方案,目前未开启cdn,方案无效

方案三:采用 "前端动态资源拦截优化" 策略

1、 核心原理:利用阿里云 OSS 免费自带的"图片处理(Image Processing)"功能,在代码中通过"全局混入"强制将所有图片转为极小的 WebP 格式。

阿里云 OSS 支持在 URL 后直接拼接处理指令。对于小程序,我们最需要的组合是:

  1. format,webp:将图片转为 WebP 格式(体积减小约 70%)。

  2. resize,w_300:按需缩放。如果是一个 100px 的头像,加载 2000px 的原图就是浪费带宽。

  3. quality,q_75:画质压缩。75% 是人眼几乎看不出区别、但体积缩减明显的平衡点。

  • 兼容性:需要对 GIF、视频、PDF 等非图片资源做自动过滤。

  • 预期效果:在不增加 CDN 费用的情况下,全站图片加载速度提升 200%-300%,流量成本降低 60%。

  • 降低质量: q_60 或 q_50

  • 调整尺寸: w_800 或 w_640

  • 移除压缩: 'image/format,webp'

?x-oss-process=image/resize,w_200,h_200,m_fill // 缩略图

?x-oss-process=image/quality,q_80 // 质量压缩

?x-oss-process=image/format,jpg // 格式转换

?x-oss-process=image/interlace,1 // 渐进显示

调研结果:已经按照oss图片增加webp处理,增加上了后缀,图片有明显变化
再次优化: 图片尺寸自适应(响应式图片)

问题 : 当前OSS处理参数固定为 w_1080 ,未根据设备屏幕适配

优化措施 : 根据设备像素比(DPR)动态调整图片尺寸

优化前:

优化后:

代码思路:

直接在u-image组件接收的src直接对src进行处理

(以下是优化过后的u-image组件,可以参考oss增加webp的逻辑)

复制代码
<template>
	<view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]">
		<image
			v-if="!isError"
			:src="processedSrc || src"
			:mode="mode"
			@error="onErrorHandler"
			@load="onLoadHandler"
			:lazy-load="lazyLoad"
			class="u-image__image"
			:show-menu-by-longpress="showMenuByLongpress"
			:style="{
				borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
			}"
		></image>
		<view
			v-if="showLoading && loading"
			class="u-image__loading"
			:style="{
				borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius),
				backgroundColor: this.bgColor
			}"
		>
			<slot v-if="$slots.loading" name="loading" />
			<u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon>
		</view>
		<view
			v-if="showError && isError && !loading"
			class="u-image__error"
			:style="{
				borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
			}"
		>
			<slot v-if="$slots.error" name="error" />
			<u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
		</view>
	</view>
</template>

<script>
/**
 * Image 图片
 * @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
 * @tutorial https://uviewui.com/components/image.html
 * @property {String} src 图片地址
 * @property {String} mode 裁剪模式,见官网说明
 * @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%)
 * @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto)
 * @property {String} shape 图片形状,circle-圆形,square-方形(默认square)
 * @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0)
 * @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true)
 * @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false)
 * @property {String} loading-icon 加载中的图标,或者小图片(默认 photo)
 * @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle)
 * @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true)
 * @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true)
 * @property {Boolean} fade 是否需要淡入效果(默认 true)
 * @property {String Number} width 传入图片路径时图片的宽度
 * @property {String Number} height 传入图片路径时图片的高度
 * @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false)
 * @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500)
 * @event {Function} click 点击图片时触发
 * @event {Function} error 图片加载失败时触发
 * @event {Function} load 图片加载成功时触发
 * @example <u-image width="100%" height="300rpx" :src="src"></u-image>
 */
export default {
	name: 'u-image',
	props: {
		// 图片地址
		src: {
			type: String,
			default: ''
		},
		// 裁剪模式
		mode: {
			type: String,
			default: 'aspectFill'
		},
		// 宽度,单位任意
		width: {
			type: [String, Number],
			default: '100%'
		},
		// 高度,单位任意
		height: {
			type: [String, Number],
			default: 'auto'
		},
		// 图片形状,circle-圆形,square-方形
		shape: {
			type: String,
			default: 'square'
		},
		// 圆角,单位任意
		borderRadius: {
			type: [String, Number],
			default: 0
		},
		// 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序
		lazyLoad: {
			type: Boolean,
			default: true
		},
		// 开启长按图片显示识别微信小程序码菜单
		showMenuByLongpress: {
			type: Boolean,
			default: true
		},
		// 加载中的图标,或者小图片
		loadingIcon: {
			type: String,
			default: 'photo'
		},
		// 加载失败的图标,或者小图片
		errorIcon: {
			type: String,
			default: 'error-circle'
		},
		// 是否显示加载中的图标或者自定义的slot
		showLoading: {
			type: Boolean,
			default: true
		},
		// 是否显示加载错误的图标或者自定义的slot
		showError: {
			type: Boolean,
			default: true
		},
		// 是否需要淡入效果
		fade: {
			type: Boolean,
			default: true
		},
		// 只支持网络资源,只对微信小程序有效
		webp: {
			type: Boolean,
			default: false
		},
		// 过渡时间,单位ms
		duration: {
			type: [String, Number],
			default: 500
		},
		// 背景颜色,用于深色页面加载图片时,为了和背景色融合
		bgColor: {
			type: String,
			default: '#f3f4f6'
		}
	},
	data() {
		return {
			// 图片是否加载错误,如果是,则显示错误占位图
			isError: false,
			// 初始化组件时,默认为加载中状态
			loading: true,
			// 不透明度,为了实现淡入淡出的效果
			opacity: 1,
			// 过渡时间,因为props的值无法修改,故需要一个中间值
			durationTime: this.duration,
			// 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
			backgroundStyle: {},
			// 处理后的图片地址(添加webp后缀)
			processedSrc: ''
		};
	},
	watch: {
		src: {
			immediate: true,
			handler (n) {
				if(!n) {
					// 如果传入null或者'',或者false,或者undefined,标记为错误状态
					this.isError = true;
					this.loading = false;
					this.processedSrc = '';
				} else {
					this.isError = false;
					// 处理OSS图片,自动添加webp后缀
					this.processedSrc = this.processOssImage(n);
				}
			}
		}
	},
	computed: {
		wrapStyle() {
			let style = {};
			// 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
			style.width = this.$u.addUnit(this.width);
			style.height = this.$u.addUnit(this.height);
			// 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值
			style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius);
			// 如果设置圆角,必须要有hidden,否则可能圆角无效
			style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible';
			if (this.fade) {
				style.opacity = this.opacity;
				style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`;
			}
			return style;
		}
	},
	methods: {
		// 点击图片
		onClick() {
			this.$emit('click');
		},
		// 图片加载失败
		onErrorHandler(err) {
			this.loading = false;
			this.isError = true;
			this.$emit('error', err);
		},
		// 图片加载完成,标记loading结束
		onLoadHandler() {
			this.loading = false;
			this.isError = false;
			this.$emit('load');
			// 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
			// 否则无需fade效果时,png图片依然能看到下方的背景色
			if (!this.fade) return this.removeBgColor();
			// 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
			this.opacity = 0;
			// 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
			// 到图片展示的过程中的淡入效果
			this.durationTime = 0;
			// 延时50ms,否则在浏览器H5,过渡效果无效
			setTimeout(() => {
				this.durationTime = this.duration;
				this.opacity = 1;
				setTimeout(() => {
					this.removeBgColor();
				}, this.durationTime);
			}, 50);
		},
		// 移除图片的背景色
		removeBgColor() {
			// 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
			this.backgroundStyle = {
				backgroundColor: 'transparent'
			};
		},
		// 处理OSS图片,自动添加webp格式后缀,并根据尺寸自适应压缩
		processOssImage(url) {
			if (!url || typeof url !== 'string') {
				return url;
			}
			// 判断是否为OSS图片(包含oss关键词或阿里云OSS域名特征)
			const isOssImage = url.includes('oss') || 
				url.includes('aliyuncs.com') || 
				url.includes('oss-cn-');
			
			if (!isOssImage) {
				return url;
			}
			
			// 如果已经包含x-oss-process参数,追加format,webp
			if (url.includes('x-oss-process')) {
				// 如果已经包含webp格式,不再处理
				if (url.includes('format,webp')) {
					return url;
				}
				// 追加webp格式转换
				return url + '/format,webp';
			}
			
			// 根据组件尺寸计算合适的图片宽度
			const targetWidth = this.getTargetWidth();
			// 根据图片尺寸和使用场景选择质量参数
			const quality = this.getQualityBySize(targetWidth);
			
			// OSS图片处理参数:自适应尺寸、转换为webp格式、动态质量
			const OSS_PROCESS_PARAMS = `image/resize,w_${targetWidth},m_lfit/format,webp/quality,q_${quality}`;
			
			// 如果没有x-oss-process参数,添加完整的处理参数
			const separator = url.includes('?') ? '&' : '?';
			return url + separator + 'x-oss-process=' + OSS_PROCESS_PARAMS;
		},
		
		// 根据组件宽度计算目标图片宽度(考虑设备像素比)
		getTargetWidth() {
			try {
				// 获取设备信息
				const systemInfo = uni.getSystemInfoSync();
				const dpr = systemInfo.pixelRatio || 1;
				const screenWidth = systemInfo.windowWidth || 375;
				
				// 解析组件宽度
				let componentWidth = this.parseWidth(this.width, screenWidth);
				
				// 根据设备像素比计算实际需要的图片宽度
				let targetWidth = Math.ceil(componentWidth * dpr);
				
				// 限制最大宽度,避免过大图片
				const MAX_WIDTH = 1080;
				const MIN_WIDTH = 100;
				
				if (targetWidth > MAX_WIDTH) {
					targetWidth = MAX_WIDTH;
				} else if (targetWidth < MIN_WIDTH) {
					targetWidth = MIN_WIDTH;
				}
				
				// 按50的倍数取整,增加缓存命中率
				return Math.ceil(targetWidth / 50) * 50;
			} catch (e) {
				// 异常情况下返回默认值
				return 800;
			}
		},
		
		// 解析组件宽度,返回px数值
		parseWidth(width, screenWidth) {
			if (typeof width === 'number') {
				// 数值类型,认为是rpx,转换为px
				return width / 2;
			}
			
			if (typeof width === 'string') {
				// 处理百分比
				if (width.includes('%')) {
					const percent = parseFloat(width) / 100;
					return screenWidth * percent;
				}
				// 处理rpx
				if (width.includes('rpx')) {
					return parseFloat(width) / 2;
				}
				// 处理px
				if (width.includes('px')) {
					return parseFloat(width);
				}
				// 纯数字字符串
				if (!isNaN(parseFloat(width))) {
					return parseFloat(width) / 2;
				}
			}
			
			// 默认返回屏幕宽度
			return screenWidth;
		},
		
		// 根据图片尺寸选择质量参数
		getQualityBySize(targetWidth) {
			// 小图标/头像:高压缩率,质量60
			if (targetWidth <= 150) {
				return 60;
			}
			// 中等尺寸图片:质量70
			if (targetWidth <= 400) {
				return 70;
			}
			// 大图:质量75(平衡清晰度与体积)
			if (targetWidth <= 800) {
				return 75;
			}
			// 超大图:质量80,保证清晰度
			return 80;
		}
	}
};
</script>

<style scoped lang="scss">
@import '../../libs/css/style.components.scss';

.u-image {
	position: relative;
	transition: opacity 0.5s ease-in-out;

	&__image {
		width: 100%;
		height: 100%;
	}

	&__loading,
	&__error {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		@include vue-flex;
		align-items: center;
		justify-content: center;
		background-color: $u-bg-color;
		color: $u-tips-color;
		font-size: 46rpx;
	}
}
</style>

方案四:后端 OSS 资源元数据强缓存优化

  1. 技术实现

在后端调用阿里云 OSS SDK 上传图片的代码逻辑中,通过设置 ObjectMetadata(对象元数据),统一为资源注入以下 HTTP 响应头:

  • 配置参数Cache-Control: max-age=31536000

  • 实施方式 :在执行 putObject(上传操作)时,全局配置 metadata.setCacheControl("max-age=31536000")

深度解析:什么是 max-age=31536000

  • 定义Cache-Control 是 HTTP 协议中控制缓存的核心指令。max-age 代表资源在客户端(用户手机)中被视为"新鲜"的最大时间,单位为秒。

  • 数值换算31536000 秒 = 3600秒 × 24小时 × 365天 = 1 年

  • 运行机制 :当小程序首次下载图片后,手机浏览器会将该图片存入本地磁盘或 内存 。在未来的一年内,只要图片 URL 不变,手机将直接从本地读取,不再向阿里云服务器发送任何网络请求。

为什么需要这样做?(核心痛点解决)

  • 消除网络排队延迟:在 HTTP/1.1 协议下,浏览器对同一域名的并发请求有限制。如果不设缓存,每次打开页面图片都要"排队下载"。开启强缓存后,图片加载跳过了网络阶段,彻底解决"1-2秒才出图"的尴尬。

  • 解决重复渲染闪烁:用户在切换页面或二次进入小程序时,由于资源已在本地,图片会随页面同步"瞬时弹出",消除先白屏、后出图的视觉闪烁感。

. 方案优势与商业价值

  • 极致的加载性能(0ms 响应) : 二次访问时,图片的加载耗时将从"秒级"直接降至 0 毫秒 (显示为 from memory cachefrom disk cache)。

  • 大幅降低运营成本(省钱) : 阿里云 OSS 是按下行流量 计费的。配置强缓存后,大量重复的图片访问不再产生外网流出流量。根据行业测算,此举可为公司节省 30% - 60% 的 OSS 流量费用。

  • 提高系统稳定性: 极大降低了高并发时期 OSS 服务器的并发压力,确保核心业务接口(如登录、支付)在高峰期拥有更多的带宽资源。

风险控制:如果图片需要更新怎么办?

  • 文件名版本号化 :对于需要更新的图片(如活动 Banner),建议在上传时修改文件名,或在前端 URL 后拼接版本号(如 ?v=20260311)。

  • 结论:由于 URL 变动会被视为新资源,手机会自动重新下载并缓存。这确保了"静态图标永久缓存,动态资源受控更新"的完美平衡。

  • 配置 max-age=3600 (1小时)

    • 优势:时效性极高。

    • 体验:用户在一个小时内点击小程序是秒开的。

    • 代价:相比 1 天,会产生更多的 OSS 流量请求。

调研结果:不建议使用,虽然0 ms 秒开图片,但是因为365天意味着图片一直未更新,运营侧上传的图片无法实时更新,如果更新为1小时配置,可以看情况考虑,但是要基于我们的运营经常就该患教或者其他图片相关配置决定

方案五: HTTP缓存头优化

问题: 静态资源缓存策略不明确

优化措施: 在OSS/服务器端配置缓存头

nginx

Nginx配置示例

location ~* \.(png|jpg|jpeg|gif|webp|svg)$ {

expires 30d; # 图片缓存30天

add_header Cache-Control "public, immutable";

add_header Vary "Accept-Encoding";

}

带hash的文件长期缓存

location ~* \.[a-f0-9]{8,}\.(png|jpg|jpeg|gif|webp)$ {

expires 1y;

add_header Cache-Control "public, immutable";

}

调研结果:缓存三十天的方案和上面的缓存也差不多,无法实时更新

方案六: 首屏关键图片预加载(使用首次获取低质量压缩图,获取到接口再展示清晰图)

问题: 首屏图片(如轮播图、logo)与页面同时加载,造成白屏

优化措施: 在App.vue中提前预加载关键图片

主要处理大尺寸的图片(封面图、头像等),小图标(20x20, 32x32, 36x36等)不需要渐进式加载(反而会更久)

首页:

IM头像

预加载组件:

复制代码
<template>
	<view class="progressive-image-wrapper" :style="{ width: imgWidth, height: imgHeight, borderRadius: imgRadius }">
		<!-- 模糊缩略图 -->
		<image
			v-if="showThumb && thumbUrl"
			class="progressive-image thumb"
			:src="thumbUrl"
			:mode="mode"
			:style="{ 
				width: imgWidth, 
				height: imgHeight, 
				borderRadius: imgRadius,
				filter: 'blur(10px)',
				transform: 'scale(1.1)'
			}"
		/>
		<!-- 骨架屏 -->
		<view 
			v-if="loading && !showThumb"
			class="progressive-skeleton"
			:style="{ 
				width: imgWidth, 
				height: imgHeight, 
				borderRadius: imgRadius,
				backgroundColor: bgColor
			}"
		>
			<view class="progressive-shimmer"></view>
		</view>
		<!-- 高清原图 -->
		<image
			class="progressive-image original"
			:src="originalUrl"
			:mode="mode"
			:style="{ 
				width: imgWidth, 
				height: imgHeight, 
				borderRadius: imgRadius,
				opacity: originalLoaded ? 1 : 0,
				transition: `opacity ${fadeDuration}ms ease-in-out`
			}"
			@load="onOriginalLoad"
			@error="onOriginalError"
		/>
	</view>
</template>

<script>
/**
 * ProgressiveImage 渐进式图片加载组件
 * @description 先加载模糊缩略图,再加载高清原图,实现渐进式加载效果
 * @property {String} src 原图URL
 * @property {String} thumb 缩略图URL(不传则自动生成)
 @property {String | Number} width 宽度(默认100%)
 * @property {String | Number} height 高度(默认200rpx)
 * @property {String | Number} borderRadius 圆角(默认0)
 * @property {String} mode 图片裁剪模式(默认aspectFill)
 * @property {String} bgColor 骨架屏背景色(默认#f0f0f0)
 * @property {Number} fadeDuration 淡入动画时长,单位ms(默认300)
 * @property {Number} thumbQuality 缩略图质量 1-100(默认30)
 * @property {Number} thumbWidth 缩略图宽度(默认200)
 * @example 
 * <progressive-image 
 *   src="https://example.com/image.jpg" 
 *   width="300rpx" 
 *   height="200rpx"
 *   borderRadius="12rpx"
 * ></progressive-image>
 */
export default {
	name: 'ProgressiveImage',
	props: {
		src: {
			type: String,
			default: ''
		},
		thumb: {
			type: String,
			default: ''
		},
		width: {
			type: [String, Number],
			default: '100%'
		},
		height: {
			type: [String, Number],
			default: '200rpx'
		},
		borderRadius: {
			type: [String, Number],
			default: 0
		},
		mode: {
			type: String,
			default: 'aspectFill'
		},
		bgColor: {
			type: String,
			default: '#f0f0f0'
		},
		fadeDuration: {
			type: Number,
			default: 300
		},
		thumbQuality: {
			type: Number,
			default: 30
		},
		thumbWidth: {
			type: Number,
			default: 200
		}
	},
	data() {
		return {
			loading: true,
			originalLoaded: false,
			thumbLoaded: false,
			loadError: false
		}
	},
	computed: {
		imgWidth() {
			return this.$u.addUnit(this.width);
		},
		imgHeight() {
			return this.$u.addUnit(this.height);
		},
		imgRadius() {
			if (this.borderRadius === 'circle' || this.borderRadius === '50%') {
				return '50%';
			}
			return this.$u.addUnit(this.borderRadius);
		},
		// 生成缩略图URL
		thumbUrl() {
			if (this.thumb) return this.thumb;
			if (!this.src) return '';
			
			// 如果是OSS图片,添加压缩参数
			if (this.src.includes('aliyuncs.com') || this.src.includes('oss-')) {
				const params = [
					`resize,w_${this.thumbWidth}`,
					'format,webp',
					`quality,q_${this.thumbQuality}`
				];
				return `${this.src}?x-oss-process=image/${params.join('/')}`;
			}
			
			return this.src;
		},
		originalUrl() {
			return this.src;
		},
		showThumb() {
			return this.thumbUrl && !this.originalLoaded && !this.loadError;
		}
	},
	watch: {
		src: {
			immediate: true,
			handler(newVal) {
				if (newVal) {
					this.loading = true;
					this.originalLoaded = false;
					this.loadError = false;
					this.preloadThumb();
				}
			}
		}
	},
	methods: {
		// 预加载缩略图
		preloadThumb() {
			if (!this.thumbUrl) return;
			
			uni.downloadFile({
				url: this.thumbUrl,
				success: () => {
					this.thumbLoaded = true;
				},
				fail: () => {
					// 缩略图加载失败,直接显示骨架屏等待原图
					this.thumbLoaded = false;
				}
			});
		},
		// 原图加载完成
		onOriginalLoad() {
			this.originalLoaded = true;
			this.loading = false;
			this.$emit('load');
		},
		// 原图加载失败
		onOriginalError() {
			this.loadError = true;
			this.loading = false;
			this.$emit('error');
		}
	}
}
</script>

<style scoped lang="scss">
.progressive-image-wrapper {
	position: relative;
	overflow: hidden;
	background-color: #f5f5f5;
}

.progressive-image {
	position: absolute;
	top: 0;
	left: 0;
	will-change: opacity, transform;
	
	&.thumb {
		z-index: 1;
	}
	
	&.original {
		z-index: 2;
	}
}

.progressive-skeleton {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 0;
	overflow: hidden;
}

.progressive-shimmer {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background: linear-gradient(
		90deg,
		transparent 0%,
		rgba(255, 255, 255, 0.4) 50%,
		transparent 100%
	);
	animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
	0% {
		transform: translateX(-100%);
	}
	100% {
		transform: translateX(100%);
	}
}
</style>

方案7: 骨架屏优化(已部分实现)

现状: 已有Skeleton组件,但未全面应用

优化建议: 在图片加载区域使用骨架屏占位

太快截图不到闪烁

骨架屏组件:

复制代码
<template>
	<!-- 图片骨架屏占位组件 -->
	<view class="image-skeleton-wrapper" :style="{ width: skeletonWidth, height: skeletonHeight, borderRadius: skeletonRadius }">
		<view 
			v-if="loading" 
			class="image-skeleton" 
			:style="{ 
				width: '100%', 
				height: '100%', 
				borderRadius: skeletonRadius,
				backgroundColor: bgColor
			}"
		>
			<view class="image-skeleton__shimmer"></view>
		</view>
		<slot v-else></slot>
	</view>
</template>

<script>
/**
 * ImageSkeleton 图片骨架屏占位组件
 * @description 在图片加载前显示骨架屏占位,提升用户体验
 * @property {String | Number} width 宽度,支持rpx、px、%(默认100%)
 * @property {String | Number} height 高度,支持rpx、px(默认200rpx)
 * @property {String | Number} borderRadius 圆角(默认0)
 * @property {Boolean} loading 是否显示骨架屏(默认true)
 * @property {String} bgColor 骨架屏背景色(默认#f0f0f0)
 * @example 
 * <image-skeleton width="200rpx" height="200rpx" borderRadius="50%" :loading="!imageLoaded">
 *   <u-image width="200" height="200" :src="imageUrl" @load="imageLoaded = true"></u-image>
 * </image-skeleton>
 */
export default {
	name: 'ImageSkeleton',
	props: {
		width: {
			type: [String, Number],
			default: '100%'
		},
		height: {
			type: [String, Number],
			default: '200rpx'
		},
		borderRadius: {
			type: [String, Number],
			default: 0
		},
		loading: {
			type: Boolean,
			default: true
		},
		bgColor: {
			type: String,
			default: '#f0f0f0'
		}
	},
	computed: {
		skeletonWidth() {
			return this.$u.addUnit(this.width);
		},
		skeletonHeight() {
			return this.$u.addUnit(this.height);
		},
		skeletonRadius() {
			if (this.borderRadius === 'circle' || this.borderRadius === '50%') {
				return '50%';
			}
			return this.$u.addUnit(this.borderRadius);
		}
	}
}
</script>

<style scoped lang="scss">
.image-skeleton-wrapper {
	position: relative;
	overflow: hidden;
}

.image-skeleton {
	position: relative;
	overflow: hidden;

	&__shimmer {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		background: linear-gradient(
			90deg,
			transparent 0%,
			rgba(255, 255, 255, 0.4) 50%,
			transparent 100%
		);
		animation: shimmer 1.5s infinite;
	}
}

@keyframes shimmer {
	0% {
		transform: translateX(-100%);
	}
	100% {
		transform: translateX(100%);
	}
}
</style>

方案5: 静态资源本地缓存策略

问题: 静态图标每次都需要网络请求

优化措施: 将常用小图标转为Base64或使用小程序本地资源

思考:我们的静态资源都是放在服务器上通过服务器路径拼接,如果常用小图标改为本地资源,是否会影响小 程序包 体积大小

相关推荐
Maimai108082 小时前
React Server Components 是什么?一文讲清 CSR、Server Components 与 Next.js 中的客户端/服务端组件
前端·javascript·css·react.js·前端框架·html·web3
2501_915921432 小时前
在 Linux 上通过命令行上架 iOS APP,Fastlane + AppUploader(开心上架)
android·linux·运维·ios·小程序·uni-app·iphone
云云只是个程序马喽2 小时前
推客系统源码部署小程序配置机构号教程
小程序
肉肉不吃 肉2 小时前
事件循环,宏任务,微任务
前端·javascript
z止于至善2 小时前
Vue ECharts:Vue 生态下的 ECharts 可视化最佳实践
前端·vue.js·echarts·vue echarts
℘团子এ2 小时前
什么是Docker
前端·docker·容器
Software攻城狮2 小时前
【el-table 表格组件 删除标头分割线】
前端·vue.js·elementui
陆康永2 小时前
vue2封装hook函数,可以监听主页面生命周期
前端·javascript·vue.js
吴声子夜歌2 小时前
小程序——开放接口(登录和用户信息)详解
小程序·apache