# 竞品 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。 - 背景图统一通过 `` 渲染。 - 图片仍继续走受保护路径解析和图片缓存链路。 取消原因是大尺寸 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' ``` 这样可以减少图片加载时的主线程和合成压力。 ### 9. 全局低端机移除高成本模糊,滚动动画保持可用 修改文件: - `src/styles/global.css` 低端机下统一移除: ```css backdrop-filter -webkit-backdrop-filter ``` 低端机模式和系统“减少动态效果”模式都不再暂停 `.scroll-wrapper` 这类跑马灯动画,避免 iOS 或低端安卓上排行榜、奖励列表、用户昵称等文本滚动完全不动。系统开启“减少动态效果”时,只暂停弹幕这类更重的装饰动画。 同时,多个活动页和榜单页的 `isLoading` 预加载阶段已取消渲染 `BackgroundLayer`、背景图片和其他装饰节点,只保留 `LoadingSpinner` 和 loading 文案。两者统一包在 `.loading-content` 容器中,使用半透明黑底、8px 圆角和 10px 内边距,避免加载态和正式页面重复创建背景图层。 ### 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。