aslan-h5/docs/活动页/分析/竞品H5与性能优化分析.md

14 KiB
Raw Blame History

竞品 H5 与本项目性能优化分析

背景

本次主要排查 src/views/Activitiessrc/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/Activitiessrc/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

缓存头为:

Cache-Control "public, max-age=2592000, immutable"

3. 图片缓存容量下调并支持单图版本

修改文件:

  • src/config/imageCacheVersion.js
  • src/config/imagePaths.js
  • src/utils/image/imageCacheManager.js

缓存容量从最多 200 张、200MB 调整为:

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 动画,并设置:

el.decoding = 'async'

同时,所有 v-smart-img 图片都会在缓存解析和真实图片加载完成前先隐藏:

el.style.opacity = '0'
el.style.transition = skipFade ? 'none' : 'opacity 0.3s ease-in-out'

加载成功后再统一显示:

el.style.opacity = '1'

这样可以避免低端机上先出现浏览器失败占位图,再替换成真实图片的问题。低端机仍然使用 transition: none,不会恢复淡入动画,不会增加持续合成成本;只是一次性隐藏、一次性显示。

9. 全局低端机移除高成本模糊,滚动动画保持可用

修改文件:

  • src/styles/global.css

低端机下统一移除:

backdrop-filter
-webkit-backdrop-filter

低端机模式和系统“减少动态效果”模式都不再暂停 .scroll-wrapper 这类跑马灯动画,避免 iOS 或低端安卓上排行榜、奖励列表、用户昵称等文本滚动完全不动。系统开启“减少动态效果”时,只暂停弹幕这类更重的装饰动画。

同时,全项目带 loading-container 的预加载阶段已取消渲染 BackgroundLayer、背景图片和其他装饰节点,只保留 LoadingContentLoadingContent 内部统一包在 .loading-content 容器中使用半透明黑底、8px 圆角和 10px 内边距,避免加载态和正式页面重复创建背景图层。

补充调整:

  • src/views/Invitation/InviteToRegister.vuesrc/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/CoupleRanking / GamePlay 一级模块。
  • 已调整 Ranking/Couple/GamePlay 内的 lottery / signIn / exchange 子模块。
  • 已调整 SpringFestivalTaskRankingRankingRewardRechargeReward 模块。

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。