358 lines
14 KiB
Markdown
358 lines
14 KiB
Markdown
# 竞品 H5 与本项目性能优化分析
|
||
|
||
## 背景
|
||
|
||
本次主要排查 `src/views/Activities` 和 `src/views/Ranking` 下活动页、榜单页在低端安卓 WebView 中打开和运行卡顿的问题。测试现象中,2GB 内存低端机在展示弹幕 `Barrage`、滚动文本、跑马灯等持续动画时明显掉帧,离开这些区域后流畅度恢复。
|
||
|
||
本次同时参考了两类竞品 H5:
|
||
|
||
- Yalla / qrsshine 活动页:
|
||
- `activity2.yalla.live/activity/2025dist/Thursday`
|
||
- `activity2.yalla.live/activity/2025dist/sea2`
|
||
- `activity2.yalla.live/activity/2025dist/userCarnival2`
|
||
- `activity2.qrsshine.com/activity/2024dist/activityMap`
|
||
- `activity2.qrsshine.com/activity/2024dist/roomHonorSystem`
|
||
- Hawatalk 活动页:
|
||
- `cdn.hawatalk.com/activitys/hexactivityprojectv3/guild-world-peak`
|
||
- `cdn.hawatalk.com/activitys/hexactivityprojectv3/miss-pk-2026`
|
||
- `cdn.hawatalk.com/activitys/hexactivityprojectv3/dynamic-user-id`
|
||
- `cdn.hawatalk.com/activitys/hexactivityprojectv3/recharge-activity`
|
||
- `cdn.hawatalk.com/activitys/hexactivityprojectv3/tower-tycoon`
|
||
- `cdn.hawatalk.com/activitys/hexactivityprojectv3/predator-journey`
|
||
|
||
## 竞品 H5 的主要特点
|
||
|
||
### 1. 活动独立包更明显
|
||
|
||
竞品活动页大多是独立活动目录,HTML 很薄,活动自身 JS 较小。Hawatalk 新旧活动页中,活动入口 JS 通常只有几十 KB 到八十多 KB,公共 vendor 大约两百多 KB。Yalla 部分页面 vendor 较大,但也是按活动目录独立部署。
|
||
|
||
这种方式的好处是:
|
||
|
||
- 单个活动的首屏代码体积更可控。
|
||
- 活动之间互相影响小。
|
||
- 活动资源可以按目录独立缓存和更新。
|
||
|
||
### 2. 静态资源缓存策略更稳定
|
||
|
||
竞品普遍使用内容 hash 文件名。HTML 短缓存或不缓存,JS/CSS/图片等静态资源长缓存。这样普通发版时,只要资源内容不变,用户端就能继续命中缓存。
|
||
|
||
我们的项目之前在构建文件名里带 `buildTimestamp`,每次构建都会导致 JS/CSS 文件 URL 改变,哪怕内容没变也会让缓存失效。这个问题已在本次优化中处理。
|
||
|
||
### 3. 动画和视觉效果相对克制0
|
||
|
||
竞品页面也会有活动视觉、排行列表和特效,但多数页面不会在首屏同时叠加大量持续动画、canvas 背景、弹幕、跑马灯、模糊滤镜和大批图片解码。
|
||
|
||
本项目部分页面中,持续动画与图片层叠加较多,在高端机上影响不明显,但低端 WebView 中会明显放大。
|
||
|
||
## 我们项目的主要差异与问题
|
||
|
||
### 1. 持续动画叠加较多
|
||
|
||
在 `src/views/Activities` 和 `src/views/Ranking` 中,存在多类持续动画:
|
||
|
||
- 弹幕 `Barrage`
|
||
- tab 文字滚动
|
||
- 奖励列表跑马灯
|
||
- 抽奖滚动动画
|
||
- 倒计时定时器
|
||
|
||
其中弹幕和跑马灯最容易在低端机上触发掉帧,因为它们会持续制造合成层,并且通常伴随头像、奖品图、渐变文字和透明背景。
|
||
|
||
### 2. `backdrop-filter` 成本高
|
||
|
||
部分页面使用了 `backdrop-filter: blur(...)`,例如:
|
||
|
||
- `src/views/Ranking/Couple/components/Barrage.vue`
|
||
- `src/views/Activities/LuckyDollars/Season5/components/Barrage.vue`
|
||
- 多个弹窗、卡片、奖励区域
|
||
|
||
`backdrop-filter` 在低端安卓 WebView 上成本很高。它会让浏览器每帧采样并合成背景,和弹幕/跑马灯一起出现时尤其容易卡顿。
|
||
|
||
### 3. canvas 背景内存压力大,已取消该方案
|
||
|
||
`BackgroundLayer` 曾支持 `useCanvas=true`,会先通过图片缓存加载图片,再把多张背景图绘制到一个 canvas 上。这个设计主要用于提高直接扒取 DOM 背景结构的门槛。
|
||
|
||
经过低端机性能排查后,该方案已取消:
|
||
|
||
- 页面继续保留 `useCanvas` 入参,兼容历史调用。
|
||
- 当前 `BackgroundLayer` 不再生成 canvas。
|
||
- 背景图统一通过 `<img v-smart-img>` 渲染。
|
||
- 图片仍继续走受保护路径解析和图片缓存链路。
|
||
|
||
取消原因是大尺寸 canvas 会占用较高内存和 GPU 资源。如果页面同时有弹幕、跑马灯、头像列表,就容易出现明显卡顿。canvas 对防扒取只有“提高一点直接复制 DOM 的门槛”的作用,防不住抓包、运行时 Hook 或最终像素提取,因此优先保证页面流畅度。
|
||
|
||
### 4. 非关键图片预加载过早
|
||
|
||
多个页面 mounted 后立即调用 `preloadOtherImages()`。虽然缓存能减少重复网络下载,但打开页面时立即预加载非关键图片,会和首屏渲染、背景绘制、接口返回后的列表渲染、弹幕动画抢资源。
|
||
|
||
因此缓存不是越激进越好。缓存能减少网络重复下载,但不能减少图片解码、DOM 渲染和 GPU 合成成本。
|
||
|
||
### 5. 部分隐藏内容仍然挂载
|
||
|
||
一些页面用 `v-show` 切换大模块。`v-show` 只是隐藏,不会卸载组件。隐藏模块中的 DOM、图片、定时器、观察器仍可能占用内存和运行成本。活动页/榜单页中如果大模块较多,低端机压力会更明显。
|
||
|
||
## 本次已经完成的优化
|
||
|
||
### 1. 构建文件名改为内容 hash
|
||
|
||
修改文件:
|
||
|
||
- `vite.config.js`
|
||
|
||
已移除构建文件名中的 `buildTimestamp`,改为内容 hash:
|
||
|
||
- `js/[hash].js`
|
||
- `css/[hash].[ext]`
|
||
- `assets/[hash].[ext]`
|
||
|
||
这样普通发版时,内容没变的 JS/CSS/图片 URL 不会改变,能继续命中浏览器/CDN 缓存。
|
||
|
||
### 2. Nginx 静态资源长缓存
|
||
|
||
修改文件:
|
||
|
||
- `nginx/default.conf`
|
||
- `nginx/default_hooka.conf`
|
||
|
||
静态资源长缓存范围从 JS/CSS 扩展到图片、字体、视频等:
|
||
|
||
- `js`
|
||
- `css`
|
||
- `png`
|
||
- `jpg`
|
||
- `jpeg`
|
||
- `gif`
|
||
- `webp`
|
||
- `svg`
|
||
- `ico`
|
||
- `ttf`
|
||
- `otf`
|
||
- `woff`
|
||
- `woff2`
|
||
- `mp4`
|
||
|
||
缓存头为:
|
||
|
||
```nginx
|
||
Cache-Control "public, max-age=2592000, immutable"
|
||
```
|
||
|
||
### 3. 图片缓存容量下调并支持单图版本
|
||
|
||
修改文件:
|
||
|
||
- `src/config/imageCacheVersion.js`
|
||
- `src/config/imagePaths.js`
|
||
- `src/utils/image/imageCacheManager.js`
|
||
|
||
缓存容量从最多 200 张、200MB 调整为:
|
||
|
||
```js
|
||
cacheLimits: {
|
||
maxItems: 120,
|
||
maxSizeMB: 80,
|
||
}
|
||
```
|
||
|
||
原因是低端机内存有限,过大的本地图片缓存会增加内存和存储管理压力。
|
||
|
||
曾评估过 `assetVersions` 单图/目录版本机制,让图片 URL 自动追加 `?iv=版本号`。但当前我们只能处理 H5 项目和 OSS 图片替换,无法稳定控制 CDN/浏览器缓存链路;如果服务端、CDN 或 WebView 仍返回旧对象,这个功能会给维护带来误判。因此该方案已取消。
|
||
|
||
使用建议:
|
||
|
||
- 普通发版:不要改全局 `version`。
|
||
- 单张图片替换:上传新文件名,并同步修改页面中的图片引用,例如 `top1RewardBg_20260515`。
|
||
- 大批量图片迁移、缓存污染:才更新全局 `version`。
|
||
- `updateTime` 只作为人工记录,不参与实际缓存命中逻辑。
|
||
|
||
### 4. 图片预加载改为限流和 idle
|
||
|
||
修改文件:
|
||
|
||
- `src/utils/image/imagePreloader.js`
|
||
|
||
优化点:
|
||
|
||
- 默认并发限制为 3。
|
||
- 去重空值和重复 URL。
|
||
- 支持 `preloadImagesIdle()`。
|
||
- 单张图片失败不阻塞整个预加载。
|
||
|
||
页面中非关键图片预加载已改为 idle:
|
||
|
||
- `src/views/Ranking/Couple/index.vue`
|
||
- `src/views/Ranking/GamesKing/index.vue`
|
||
- `src/views/Activities/LesserBairam/index.vue`
|
||
- `src/views/Activities/SpringFestival/index.vue`
|
||
- `src/views/Activities/LuckyDollars/Season3/index.vue`
|
||
- `src/views/Activities/LuckyDollars/Season4/index.vue`
|
||
- `src/views/Activities/LuckyDollars/Season5/index.vue`
|
||
|
||
### 5. App 先挂载,再后台检查版本
|
||
|
||
修改文件:
|
||
|
||
- `src/main.js`
|
||
|
||
之前版本检查会在 app mount 前执行,可能影响打开速度。现在先挂载页面,再通过 idle/timeout 后台检查版本。
|
||
|
||
### 6. 新增通用低端机性能模式
|
||
|
||
新增文件:
|
||
|
||
- `src/utils/performanceMode.js`
|
||
|
||
低端机判断不针对单一型号,而是通用条件:
|
||
|
||
- `navigator.deviceMemory <= 2`
|
||
- 安卓低核心设备
|
||
- 旧安卓低核心 WebView
|
||
- 用户系统开启 `prefers-reduced-motion`
|
||
|
||
启动时会给 `html` 添加:
|
||
|
||
- `low-performance-mode`
|
||
- `reduce-motion-mode`
|
||
|
||
这些 class 用于全局 CSS 和组件内降级。
|
||
|
||
### 7. 取消 canvas 背景渲染
|
||
|
||
修改文件:
|
||
|
||
- `src/components/BackgroundLayer.vue`
|
||
|
||
当前即使页面传入 `useCanvas=true`,也会渲染为普通图片背景。这样仍然走图片缓存和受保护路径解析,但避免大 canvas 带来的额外内存和绘制成本。
|
||
|
||
`useCanvas` 入参暂时保留,避免大量历史页面需要同步改模板;后续新页面不再依赖它做防扒取。
|
||
|
||
### 8. `v-smart-img` 低端机图片显示策略
|
||
|
||
修改文件:
|
||
|
||
- `src/directives/smartImage.js`
|
||
|
||
低端机下关闭图片 opacity fade 动画,并设置:
|
||
|
||
```js
|
||
el.decoding = 'async'
|
||
```
|
||
|
||
同时,所有 `v-smart-img` 图片都会在缓存解析和真实图片加载完成前先隐藏:
|
||
|
||
```js
|
||
el.style.opacity = '0'
|
||
el.style.transition = skipFade ? 'none' : 'opacity 0.3s ease-in-out'
|
||
```
|
||
|
||
加载成功后再统一显示:
|
||
|
||
```js
|
||
el.style.opacity = '1'
|
||
```
|
||
|
||
这样可以避免低端机上先出现浏览器失败占位图,再替换成真实图片的问题。低端机仍然使用 `transition: none`,不会恢复淡入动画,不会增加持续合成成本;只是一次性隐藏、一次性显示。
|
||
|
||
### 9. 全局低端机移除高成本模糊,滚动动画保持可用
|
||
|
||
修改文件:
|
||
|
||
- `src/styles/global.css`
|
||
|
||
低端机下统一移除:
|
||
|
||
```css
|
||
backdrop-filter
|
||
-webkit-backdrop-filter
|
||
```
|
||
|
||
低端机模式和系统“减少动态效果”模式都不再暂停 `.scroll-wrapper` 这类跑马灯动画,避免 iOS 或低端安卓上排行榜、奖励列表、用户昵称等文本滚动完全不动。系统开启“减少动态效果”时,只暂停弹幕这类更重的装饰动画。
|
||
|
||
同时,全项目带 `loading-container` 的预加载阶段已取消渲染 `BackgroundLayer`、背景图片和其他装饰节点,只保留 `LoadingContent`。`LoadingContent` 内部统一包在 `.loading-content` 容器中,使用半透明黑底、8px 圆角和 10px 内边距,避免加载态和正式页面重复创建背景图层。
|
||
|
||
补充调整:
|
||
|
||
- `src/views/Invitation/InviteToRegister.vue` 和 `src/views/Invitation/inviteNewUser.vue` 的加载态移除 `BackgroundLayer`。
|
||
- `src/views/Activities/DailyRecharge/index.vue` 主内容改为 `v-else`,避免加载态期间提前挂载背景图和 VAP。
|
||
- `src/views/CPReward/index.vue` 主内容由 `v-show="!isLoading"` 改为 `v-else`,避免加载态期间提前创建图片列表。
|
||
|
||
### 10. 弹幕组件专项优化
|
||
|
||
修改文件:
|
||
|
||
- `src/views/Activities/LuckyDollars/Season3/components/Barrage.vue`
|
||
- `src/views/Activities/LuckyDollars/Season4/components/Barrage.vue`
|
||
- `src/views/Activities/LuckyDollars/Season5/components/Barrage.vue`
|
||
- `src/views/Ranking/Couple/components/Barrage.vue`
|
||
|
||
优化点:
|
||
|
||
- 低端机弹幕最大同时显示数降到每轨 1 条。
|
||
- 低端机弹幕生成间隔至少 4500ms。
|
||
- 页面隐藏时暂停弹幕并清空当前动画 DOM。
|
||
- 弹幕区域离开视口时暂停并清空当前动画 DOM。
|
||
- 去掉 `watch(..., { deep: true })`,避免深度监听大数组。
|
||
- 弹幕文字在入轨时预计算,减少模板每次渲染重复计算。
|
||
- 动画结束时直接从轨道移除,不再先标记 completed 再由 interval 清理。
|
||
- Couple 弹幕在低端模式下隐藏中间徽章图,减少一张持续参与动画的图片。
|
||
|
||
## 后续建议
|
||
|
||
### 1. 优先把大模块从 `v-show` 改为按需挂载
|
||
|
||
活动页中排行榜、玩法、奖励、弹窗等大模块,如果不是首屏必须展示,建议逐步改成:
|
||
|
||
- 首次进入时 `v-if` 挂载。
|
||
- 数据和图片进入模块时再加载。
|
||
- 离开模块后保留必要状态,释放重图片 DOM。
|
||
|
||
本次已按体验权衡做了选择性调整:
|
||
|
||
- 高频按钮、轻量图片状态仍保留 `v-show`,避免点击切换时闪烁。
|
||
- 会触发接口、预加载、定时器或 `IntersectionObserver` 的大组件改为 `v-if`。
|
||
- 已调整 `Ranking/Couple` 的 `Ranking` / `GamePlay` 一级模块。
|
||
- 已调整 `Ranking/Couple/GamePlay` 内的 `lottery` / `signIn` / `exchange` 子模块。
|
||
- 已调整 `SpringFestival` 的 `Task`、`Ranking`、`RankingReward`、`RechargeReward` 模块。
|
||
|
||
### 2. 继续压缩活动页图片
|
||
|
||
建议优先处理:
|
||
|
||
- 背景长图
|
||
- 弹窗背景
|
||
- 奖励图
|
||
- 大面积 PNG
|
||
|
||
能用 WebP 的尽量用 WebP。静态装饰图可以做多尺寸或压缩版,低端机使用轻量图。
|
||
|
||
### 3. 跑马灯文本尽量避免模板内测量 DOM
|
||
|
||
例如 `isTextOverflow(text)` 这类函数,如果在模板中直接调用,并且内部创建 DOM + `getBoundingClientRect()`,容易触发布局计算。建议后续改成:
|
||
|
||
- 数据变化后统一测量一次。
|
||
- 缓存结果。
|
||
- 低端机直接省略号展示,不滚动。
|
||
|
||
### 4. 弹幕建议抽成公共组件
|
||
|
||
现在 Season3/4/5 和 Couple 都有独立弹幕组件。后续可以抽一个公共 `PerformanceBarrage`,不同活动只传渲染内容和样式配置,避免同类性能修复重复做多次。
|
||
|
||
### 5. 真机验证顺序
|
||
|
||
建议在低端安卓 WebView 上按下面顺序对比:
|
||
|
||
1. 打开 LuckyDollars Season5 首页,观察弹幕区域 FPS。
|
||
2. 进入/离开弹幕区域,确认离开视口后不再持续掉帧。
|
||
3. 打开 Couple 首页,观察告白墙弹幕和顶部跑马灯。
|
||
4. 切换 Ranking / GamePlay,观察内存是否持续上涨。
|
||
5. 冷启动和二次打开分别测试,确认缓存命中后网络变少,同时页面不卡顿。
|
||
|
||
## 构建验证
|
||
|
||
已使用项目依赖执行生产构建,构建通过。
|
||
|
||
仍保留两个历史警告,和本次优化无直接关系:
|
||
|
||
- `Azizi/secondBg.png` 构建时未解析,会保留到运行时解析。
|
||
- `src/utils/http.js` 同时被静态和动态导入,无法单独拆到动态 chunk。
|