# 竞品 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。
