互动游戏团队如何将性能体验优化做到TOP级别

一、背景

随着互动游戏业务 DAU 量级增加,性能和体验重要性也越发重要,好的性能和体验不仅可以增加用户使用体感,也可以增加用户对于互动游戏的使用粘性。

对现状分析,主要存在首屏渲染速度慢、打开页面存在白屏、页面加载过多资源等问题,核心手段是增加骨架、接口优先级调整、预渲染、减小包体积等。

优化后,互动游戏签到功能做到同类业务性能体验 Top 级别,下面是优化后数据:

  • 首屏渲染速度:优化后提升首屏渲染速度 39%
  • 首屏骨架:骨架体积大小减少 44%(压缩后减少 50%)。
  • 首次加载总资源:资源总体积优化后,大小减少 69%

二、骨架

骨架屏是指在页面加载时,临时显示出页面的主要结构,可以让用户在等待页面加载完成时,得到视觉上的反馈,提升页面的用户体验。

100.jpeg

骨架示意图vs数据渲染

109.jpeg

108.jpeg

可以看出在接口返回数据之前,可以先使用骨架得到一些界面反馈。

三、缓存

虽然骨架屏可以让用户在视觉上得到反馈,毕竟不是真实的数据,总体还是有一些简陋,用户也可能并不知道这块区域实际渲染的是什么样的内容,若是网络环境不好,很可能会长时间的停留在骨架屏阶段,为了增强一些体感,使用缓存进一步对页面进行优化。

107.jpeg

使用缓存渲染具备以下优势:

  • 与骨架屏相比,缓存渲染十分接近用户最终所见,因为每次接口返回数据都会更新缓存,用户再次进入时看到的都是自己上次进入时的数据。
  • 当用户处在弱网或者断网等不可抗力的环境中时,可以得到较为完整的页面数据展示,可以很好减弱用户环境带来的网络营销。

使用缓存注意事项:

  • 一些缓存渲染应屏蔽事件响应,避免造成不必要的报错和客诉。比如商品的缓存渲染,由于商品存在下架、优惠券调整等情况,缓存的数据和实际数据会存在一定的偏差。
  • 缓存渲染逻辑需要更加前置,不应该将缓存渲染的逻辑放在原本的位置,这样会拖慢渲染的时机。

四、接口后置

浏览器对同一时间内的请求数量是有限制的,既并发请求限制。当一个页面首次渲染时需要浏览器发起很多接口请求,用于填充页面渲染需要的数据,若是对于页面渲染时的请求数量不加以控制,便可能导致一些问题出现。

现在有 home 和 info 两个接口,home 接口返回的数据是首屏渲染需要依赖的,info 接口返回的数据则不是首屏必须依赖的。假设现在还有一些其他请求占据了并发请求限制的数量,导致 home 接口请求变慢。

105.jpeg

若是 info 接口响应慢,长时间占据这浏览器的请求进程,会导致页面首屏渲染速度更慢,那么就需要有个一套方案可以根据接口的优先级进行加载顺序控制,可以将顺序变为如下。

104.jpeg

方案: 当页面加载完成后一定时间后,进行低优先级接口的请求,或者触发页面的滚动、点击等时立即进行接口请求。

此方案适用于:确定接口延迟加载并不会阻塞用户的交互和操作。

将其封装为一个 hooks,便于复用,直接先看代码再解释:

import { useRM, createRM } from 'xxx'

const listen = (type: string, listener: () => void) => {
  const l = () => {
    listener()
    document.removeEventListener(type, l)
  }
  document.addEventListener(type, l)
}

const pageFlowModule = createRM(
  {
    assemble(state) {
      const reactionObserver = () => {
        state.isUserReactioned = true
      }

      ;['scroll', 'mousedown', 'touchstart'].forEach((type) => {
        listen(type, reactionObserver)
      })
      setTimeout(reactionObserver, 4000)
    },
  },
  { isUserReactioned: false },
)

pageFlowModule.actions.assemble()

export const usePageFlow = () => {
  const [state] = useRM(pageFlowModule)
  return state
}

使用:

import { usePageFlow } from 'xxx'

const Demo = () => {
    const { isUserReactioned } = usePageFlow()

    const fetchHanlder = useCallback(() => {
        // 接口请求数据
    }, [])

    useEffect(() => {
        if(isUserReactioned) {
            fetchHanlder()
        }
    }, [isUserReactioned, fetchHanlder])

    return <div>{/* 渲染接口返回的数据 */}</div>
}

从上面代码可以看到,会将一些非首屏需要的请求后置,后置的接口可以在页面加载完成 4s 后自动触发调用,也会在用户有触屏、滚动页面等行为的时触发接口的调用。

五、骨架优化

签到和许愿树目前主文档中除了骨架部分还包含了一些公共的 JS 和 CSS,对不同资源类型进行拆分、汇总后发现,不管是签到还是许愿树,实际包含 HTML + JS 部分仅占极小比例,大量的流量消耗在了 CSS 上。

对 HTML 中 CSS 部分再进行梳理发现,文件中包含的除了骨架的 CSS 部分和公共组件库的 CSS 部分之外,还包含了大量弹框的 CSS。这三类中,骨架的 CSS 要保留,公共组件库的 CSS 可以拆分但是难度较大,剩下的就是弹框或者非骨架部分的 CSS。

  • 需要把弹框部分组件做异步加载,保证预渲染的时候这部分 CSS 文件不会被加载到。
  • 拆分骨架组件,把骨架组件从业务组件中剥离,预渲染的时候只渲染和加载骨架部分,不加载其余主文件部分 CSS,进一步缩小骨架。

    102.jpeg

六、localStorage性能问题

在做优化之前,并未意识到 localStorage 所隐藏的性能问题,业务中使用了大量的本地存储,使用 Performance 记录一下存储消耗的时间。

记录核心代码:

export const setMallFlowStoreData = (data: any) => {
  performance.mark('start_localstorage_operation')
  // localStorage 操作.....
  performance.mark('end_localstorage_operation')

  performance.measure('localstorage_operation_duration', 'start_localstorage_operation', 'end_localstorage_operation')
}

输出记录的时间:

const entries = performance.getEntriesByName('localstorage_operation_duration')
const TOTAL_TIME = entries.reduce((current, next) => {
return current + next?.duration
}, 0)

console.log('全部记录:', entries, '共耗时:', TOTAL_TIME)

输出结果:

可以看到通过 localStorage 进行一次存储操作,大致需要耗时 0.2-0.5ms之间,若是当页面存在大量的前端的存储操作时,低端机型在存储操作上消耗甚至达到 10-20ms,若是代码写的不合理,导致页面 reload、反复触发获取操作等情况,这个时间又将会成倍的增加。

接下来先一起看看为何会存在性能方面的问题和解决方案。

存储数据

问题:

localStorage 的存储是同步的操作,因此在存储大量数据时,可能会导致阻塞 UI 线程,影响用户体验。

方案:

核心思路便是将同步操作转换为异步操作,这样就不会阻塞 UI 线程。

  • 使用 Web Worker ,会增加一些项目维护的复杂度,且其是 HTML5 标准中新增的技术,存在一定的兼容性(ChatGPT 给的,应该是错误答案,并未在 MDN 中看到)。

201.png

  • 使用 setTimeout、setInterval,兼容性绝对的好,但是并未从根本解决问题。
  • 不用 localStorage,直接上 IndexDB,但是由于代码项目原因,不能改动原有的太多逻辑。

综合解决方案和历史原因,只能退而求其次选择 setTimeout 的方式解决这个问题。

读取数据

问题:

每次读取 localStorage 数据时,都需要从磁盘中读取数据,因此在处理大量数据时,可能会出现性能问题。

方案:

可以将数据进行放到内存中缓存处理,在用户的整个操作周期内只从 localStorage 获取一次数据,需要注意的是每次对数据进行操作时,需要将 localStorage 和内存缓存的数据同步更新。

数据类型转换

问题:

在存储和读取数据时,需要将数据进行序列化和反序列化操作。这些操作可能会导致性能问题。

方案:

使用 JSON.stringify() 和 JSON.parse() 函数来处理数据的序列化和反序列化。

经过对 localStorage 存储优化以后,在红米 note 11 上面进行了简单测试,首屏打开速度提升,对于整体提升首屏提升约 2%

七、动效执行时机

页面存在渐入渐现的动效,在页面首次加载时,由于渐现动效的存在,会延迟用户感知该模块,从而导致感觉页面存在更多时间的白屏,动效如下: 

203.gif

核心问题是首次渲染直出 DOM 结构,不走渐现动效便可,这个比较偏向于逻辑处理,属于体验优化的范畴,主打的就是在后续有相关首屏动效时,有意识对其做一下处理,保证首屏首次渲染的完整度。

八、渲染模块的取舍

首先看一下两种状态各自的样式:未签到 VS 已签到。

204.png

签到业务的日历会根据用户当天签到状态进行渲染,存在已签到和未签到两种渲染逻辑,由于当前的架构限制,并不能在预渲染时感用户的签到状态,导致日历部分的渲染会滞后,严重影响页面的首屏渲染速度。

第一版本优化

将签到状态进行缓存,当用户进入签到时的大致流程如下:

207.jpeg

当用户进入页面时,会优先获取缓存中的数据进行渲染,确保用户可以第一时间看到日历部分的渲染,这里需要注意:1. 缓存需要结合用户 token 一起判断,避免造成切换账号时造成数据污染。2. 若是用户第一次进入或者当天未签到,会使用系统时间作为小日历上的数字展示,当用户修改了系统时间设置时,日期判断会存在误差。

缓存数据必然会先于接口响应数据,因此页面第一时间看到的肯定是缓存数据(没有缓存数据,会默认使用未签到数据)所渲染的页面,那么当接口响应完成时,需要使用真实的数据触发页面的 rerender,需要注意处理,避免造成页面闪烁。

虽然这样做可以提高页面的渲染体感,当进入页面时,顶部区域还是会存在一定时间的空白,毕竟还是需要执行 JS 后才能执行骨架渲染逻辑,本质提升速度为:接口响应时间 - JS 执行时间,在低端机表现会较为好一些,高端机体感并非太明显。

第二版优化

日历部分由于已签到和未签到的样式存在着较大的出入,不能像某些竞品一样:已签、未签的整体页面布局并未有区分,使用一套公用的渲染逻辑,这样也导致签到业务需要将渲染日历部分的动作滞后,那么核心就是怎么解决这个问题。

综合考虑后,决定将未签到样式作为预渲染时直接生成 DOM,这样可以保证用户未签到的状态下进入到页面可以第一时间对的状态,也可以更快的完成首屏的渲染。

若是用户已签到,便在此基础之上复用今日签到的逻辑,就是会在签到完成后展示一个小的动效,将小日历变成大日历的样式。这样做的好处可以是获取到用户真实状态后,自动切换到大日历状态,效果如下。

309.gif

结合用户行为分析:多数用户一天不会多次访问,也就是在即不怎么牺牲高频率访问用户的体验之下,提高了绝大多数用户的体验。

九、首屏数据优先请求

前置小知识:最大并发请求数

为了避免浏览器过度占用系统资源,浏览器对于同一域名下的请求数量是有一定限制的,也就是常见的浏览器最大请求数量。

以 Chrome 浏览器举例:同一域名下,HTTP 协议最多允许同时存在 6 个 TCP 连接进行,HTTPS 协议最多为 4 个。

业务现状

签到进入页面共计加载许多接口。

其中首屏渲染需要的几个核心接口如图红色标记所示,核心的接口滞后会导致页面数据渲染的更慢,严重影响体验,那么到底影响多少呢?可以在浏览器 Network 中查看 Waterfall。

304.png

核心接口是在其他完成后开始,是因为其没有赶上浏览器第一批次接口请求队列中,需要等待前面某些接口结束后,才会将其放到请求队列中。

动作

有了问题,接下来便是如何做:

  • 首先是制定方案,如何确保接口的请求可以搭上浏览器请求队列的第一班车,本质是将之前散落在各个组件内的 useEffect 中的初始化逻辑进行提取,统一触发。
  • 梳理接口和首屏渲染的关联度,确定哪些接口的优先级权重更高。

核心代码如下:

export const StartModule = createRM(
  {
    init() {
      SigninTopModule?.actions?.getHomeData()
      AdModule?.actions?.reqAdInfoList()
      HomeModule?.actions?.getBubbleList()
    },
  }
)

在页面初始化时执行 StartModule?.actions?.init(),将核心接口优化执行,通过控制接口请求顺序,签到业务在此提升了大致 6-8% 的首屏渲染速度。

十、字体使用和优化

字体加载和优化是前端开发中的一个重要问题,特别是在移动端和低网络状况下。下面是一些字体加载和优化的技巧。

FOUT问题

通过设置 Font-Display 属性可以控制字体加载时的显示效果,包括 Auto、Swap、Block、FallBack 和 Optional 几种模式,可以减少字体加载时间和防止文本闪烁。

设置属性为FallBack时效果: 

33.gif

可以看到日期存在明显的 FOUT(无样式文本闪现)问题,设置 Swap 也是类似效果,并不符合预期。

设置属性为 Block 时效果: 

22.gif

可以看到第一时间并没有渲染日期,而是有点的短暂空白,因为其可以避免 FOUT,字体文件必须在后台下载完全后,文本才能显示。

最终选择了 font-display: block;效果会更好一些。

注意,并不是整个页面都使用 Block 属性,对于一些非首屏关键渲染的样式,使用 fallback 更为合适一些,因为其会使用浏览器默认字体,所以还是需要结合业务、场景合理使用。

字体库大小,你得懂

先看一个 GPT 对于签到业务常用字体库打下的统计:

DIN Condensed 字体库的大小在几百KB 到几MB之间 Helvetica Neue 字体库的大小在几MB到十几MB之间

也就是这两种字体的大小,如果不加以处理,全部加载的大小在几 MB 到十几 MB 之间,对于前端项目而言,这是挺夸张的一件事。

可以和设计人员沟通,将字体库中常用的字体导出,前端项目仅仅引入需要的字体就好,比如 DIN Condensed 字体都是使用在阿拉伯数字上,并不会在其他字上使用,那么只需要将阿拉伯数字导出即可。比如汉字,根据《现代汉语通用字表》(GB/T 13000-2018),常用汉字(包括简体字和繁体字)共计 3500 个,其中常用的一般是指前 1000 个左右的汉字,那么在使用字体库的时候,是不是可以默认只需要导出部分即可。

经过处理后的字体库大小如下图:

0981.png

字体库数量,你得控制

上面说了一个字体库的大小是多大,就算是经过处理,最少也会有 30KB 大小,所以项目引入的字体种类是需要控制的,不能设计同学使用了多少种类字体设计,我们就要照单全收。

当设计同学新增字体库时,如果字体使用在 3 次以内,是不是可以使用图片来代替文字,或者使用现有的字体库来平替。

十一、慎用三方库

业务中存在一些简单的校验、转换和动效并不需要引入三方库,尤其是因为一个较为简单的功能引入了一个较为大且冷门的库时,不仅会增加项目的打包体积,还会增加项目后续维护的沟通、学习成本。

例如下面一个简单切换动效: 

210.gif

是一个比较常规的切换动效,却在项目中引入了一个第三方库来实现,该库的使用也是有一些学习成本,因为其具备实现比较复杂的动效能力,在业务动效具备一定复杂度且非首屏的场景下,是可以考虑引入使用的,否则类似这种首屏便需要加载的动效,还是慎重。

上述的切换动效 CSS 实现代码如下:

@keyframes bigScale {
  0% {
    opacity: 0;
    transform: scale(0.95);
  }

  to {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes smallScale {
  0% {
    transform: scale(1);
    opacity: 1;
  }

  to {
    transform: scale(0.95);
    opacity: 0;
  }
}

.squareInCenter {
  animation: 0.3s linear 0s 1 normal forwards running bigScale;
}

.squareOutCenter {
  animation: 0.3s linear 0s 1 normal forwards running smallScale;
}

在业务开发的过程中,尤其是 C 端的页面,在实现功能时对于引入额外的库是一件需要十分谨慎的事情,在内部就看到不少项目在引入关于日期处理方面的库时,DayJS、MomentJS 同时都会引用到项目中,B 端项目都不能忍,更何况 C 端项目。

十二、总结

本文仅仅介绍得物前端增长团队在互动游戏侧一些体验优化实践心得,后续还在不断迭代和优化,将实践经验应用扩大至多个业务中,将整个互动游戏性能体验优化至 TOP 级别。

*文/来骏

本文属得物技术原创,更多精彩文章请看:得物技术官网

未经得物技术许可严禁转载,否则依法追究法律责任!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/404549.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

app测试必掌握的核心测试:UI、功能测试!

一、UI测试 UI即User Interface (用户界面)的简称。UI 设计则是指对软件的人机交互、操作逻辑、界面美观的整体设计。好的UI设计不仅是让软件变得有个性有品味,还要让软件的操作变得舒适、简单、自由、充分体现软件的定位和特点。手机APP从启动界面开始, 到运行过程,直至退出,…

聊聊mysql的七种日志

进入正题前,可以先简单介绍一下,MySQL的逻辑架构, MySQL的逻辑架构大致可以分为三层: 第一层:处理客户端连接、授权认证,安全校验等。第二层:服务器 server 层,负责对SQL解释、分析、优化、执行操作引擎等。第三层:存储引擎,负责MySQL中数据的存储和提取。我们要知道…

云图极速版限时免费活动

产品介绍 云图极速版是针对拥有攻击面管理需求的用户打造的 SaaS 应用&#xff0c;致力于协助用户发现并管理互联网资产攻击面。 实战数据 (2023.11.6 - 2024.2.23) 云图极速版上线 3 个月以来&#xff0c;接入用户 3,563 家&#xff0c;扫描主体 19,961 个&#xff0c;累计发…

OpenCV笔记4:级联分类器实现嘴部检测

OpenCV 嘴部检测 """ 嘴部区域检测 1. 静态图像检测嘴部区域创建分类器加载特征文件检测图像绘制嘴部区域显示 2. 切换为摄像头 """ import cv2 import numpy as npclass FaceDetect:def __init__(self):# 级联分类器# 创建级联分类器&#xf…

云原生之容器管理工具Portainer

1. 简介 前面文章我们讲Docker、Docker Compose和Docker Swarm都是在Linux系统上手工命令行去操作&#xff0c;在第一次安装的时候可以命令行&#xff0c;以后运维和CICD流程操作中&#xff0c;如果还要命令行去各个节点操作&#xff0c;操作就麻烦了&#xff0c;工作效…

Seata 入门知识

目录 概述 工作流程 工作模式 AT模式 TCC模式 概述 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。 AT模式是阿里首推…

Linux系统运维:离线安装sar-性能监视和分析工具

目 录 一、前言 二、系统环境 三、安装sar &#xff08;一&#xff09;准备工作 1、下载 sar 工具的安装包&#xff1a; 2、将安装包传输到 CentOS 服务器 &#xff08;二&#xff09;安装工作 1、解压 2、配置安装 3、编译 4、安装 &#xff08;三&#xff0…

C# Onnx 使用onnxruntime部署实时视频帧插值

目录 介绍 效果 模型信息 项目 代码 下载 C# Onnx 使用onnxruntime部署实时视频帧插值 介绍 github地址&#xff1a;https://github.com/google-research/frame-interpolation FILM: Frame Interpolation for Large Motion, In ECCV 2022. The official Tensorflow 2…

【Flink集群RPC通讯机制(四)】集群组件(tm、jm与rm)之间的RPC通信

文章目录 1. 集群内部通讯方法概述2. TaskManager向ResourceManager注册RPC服务3. JobMaster向ResourceManager申请Slot计算资源 现在我们已经知道Flink中RPC通信框架的底层设计与实现&#xff0c;接下来通过具体的实例了解集群运行时中组件如何基于RPC通信框架构建相互之间的调…

大数据 - Spark系列《十一》- Spark累加器详解

Spark系列文章&#xff1a; 大数据 - Spark系列《一》- 从Hadoop到Spark&#xff1a;大数据计算引擎的演进-CSDN博客 大数据 - Spark系列《二》- 关于Spark在Idea中的一些常用配置-CSDN博客 大数据 - Spark系列《三》- 加载各种数据源创建RDD-CSDN博客 大数据 - Spark系列《…

2024/02/23

使用消息队列完成两个进程间相互通信 A.c #include<myhead.h> struct msgbuf {long mtype;char mtext[1024]; }; //定义表示正文内容大小的宏 #define MSGSIZE sizeof(struct msgbuf)-sizeof(long)int main(int argc, const char *argv[]) {//创建一个key值key_t key;ke…

知乎66条高赞回答,句句醍醐灌顶!

-01- 穷人是小心翼翼地大方&#xff0c; 有钱人是大大方方地小气。 ——论如何判断一个人是真有钱还是装有钱 -02- 枕头要常晒&#xff0c; 因为里面装满了心酸的泪和发霉的梦。 ——一切终将随风而逝 -03- 人活得累&#xff0c;一是太认真&#xff0c;二是太想要。 …

第3部分 原理篇2去中心化数字身份标识符(DID)(3)

3.2.2.4. DID文档 (DID Document) 本聪老师&#xff1a;DID标识符和DID URL还都只是ID&#xff0c;必须为它附加一个基本属性才可以证明是该主体独有的。这个就是我们下面介绍的DID文档。 本聪老师&#xff1a;每个DID标识符都唯一对应一个DID文档&#xff0c;也可以说&#x…

计算机功能简介:EC, NVMe, SCSI/ISCSI与块存储接口 RBD,NUMA

一 EC是指Embedded Controller 主要应用于移动计算机系统和嵌入式计算机系统中&#xff0c;为此类计算机提供系统管理功能。EC的主要功能是控制计算机主板上电时序、管理电池充电和放电&#xff0c;提供键盘矩阵接口、智能风扇接口、串口、GPIO、PS/2等常规IO功能&#xff0c;…

docker自定义网络实现容器之间的通信

Background docker原理 docker是一个Client-Server结构的系统&#xff0c;Docker的守护进程运行在主机上。通过Socket从客户端访问。docker核心三大组件&#xff1a;image–镜像、container-容器、 repository-仓库。docker使用的cpu、内存以及系统内核等资源都是直接使用宿主…

A Novel Two-Layer DAG-based Reactive Protocol for IoT Data Reliability in Metaverse

在IOT 场景中&#xff0c;需要保证数据的完整性和可靠性。通常区块链可以用来做这件事&#xff0c;但是IoT 设备的计算能力和贷款都是有限的。 对于PBFT 要求的通信量太大。 本文提出的 two layer directed acycle graph (2LDAG) 是一种被动共识协议&#xff0c;除非有节点主动…

快速构建 Debezium MySQL Example 数据库

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

EXCEL 在列不同单元格之间插入N个空行

1、第一步数据&#xff0c;要求在每个数字之间之间插入3个空格 2、拿数据个数*&#xff08;要插入空格数1&#xff09; 19*4 3、填充 4、复制数据到D列 5、下拉数据&#xff0c;选择复制填充这样1-19就会重复4次 6、全选数据D列排序&#xff0c;这样即完成了插入空格 以…

SQL语法-DQL-测试练习

因篇幅原因&#xff0c;本篇承接此篇->第八篇&#xff1a;SQL语法-DQL-数据查询语言-CSDN博客 本篇是对于SQL语法DQL语句的练习&#xff0c;因水平和精力有限&#xff08;就不像前两篇的DDL&#xff0c;DML那样自出练习了&#xff09;直接照搬了【黑马程序员】在哔哩哔哩的…

基于卷积神经网络的图像去噪

目录 背影 卷积神经网络CNN的原理 卷积神经网络CNN的定义 卷积神经网络CNN的神经元 卷积神经网络CNN的激活函数 卷积神经网络CNN的传递函数 基于卷积神经网络的图像去噪 完整代码:基于卷积神经网络的图像去噪.rar资源-CSDN文库 https://download.csdn.net/download/abc9918351…