一、背景
随着互动游戏业务 DAU 量级减少,性能和体验重要性也越发重要,好的性能和体验不仅能够减少用户应用体感,也能够减少用户对于互动游戏的应用粘性。
对现状剖析,次要存在首屏渲染速度慢、关上页面存在白屏、页面加载过多资源等问题,外围伎俩是减少骨架、接口优先级调整、预渲染、减小包体积等。
优化后,互动游戏签到性能做到同类业务性能体验 Top 级别,上面是优化后数据:
- 首屏渲染速度:优化后晋升首屏渲染速度 39%。
- 首屏骨架:骨架体积大小缩小 44%(压缩后缩小 50%)。
- 首次加载总资源:资源总体积优化后,大小缩小 69%。
二、骨架
骨架屏是指在页面加载时,长期显示出页面的次要构造,能够让用户在期待页面加载实现时,失去视觉上的反馈,晋升页面的用户体验。
骨架示意图 vs 数据渲染
能够看出在接口返回数据之前,能够先应用骨架失去一些界面反馈。
三、缓存
尽管骨架屏能够让用户在视觉上失去反馈,毕竟不是实在的数据,总体还是有一些简陋,用户也可能并不知道这块区域理论渲染的是什么样的内容,若是网络环境不好,很可能会长工夫的停留在骨架屏阶段,为了加强一些体感,应用缓存进一步对页面进行优化。
应用缓存渲染具备以下劣势:
- 与骨架屏相比,缓存渲染非常靠近用户最终所见,因为每次接口返回数据都会更新缓存,用户再次进入时看到的都是本人上次进入时的数据。
- 当用户处在弱网或者断网等不可抗力的环境中时,能够失去较为残缺的页面数据展现,能够很好削弱用户环境带来的网络营销。
应用缓存注意事项:
- 一些缓存渲染应屏蔽事件响应,防止造成不必要的报错和客诉。比方商品的缓存渲染,因为商品存在下架、优惠券调整等状况,缓存的数据和理论数据会存在肯定的偏差。
- 缓存渲染逻辑须要更加前置,不应该将缓存渲染的逻辑放在本来的地位,这样会拖慢渲染的机会。
四、接口后置
浏览器对同一时间内的申请数量是有限度的,既并发申请限度。当一个页面首次渲染时须要浏览器发动很多接口申请,用于填充页面渲染须要的数据,若是对于页面渲染时的申请数量不加以控制,便可能导致一些问题呈现。
当初有 home 和 info 两个接口,home 接口返回的数据是 首屏渲染须要依赖 的,info 接口返回的数据则 不是首屏必须依赖 的。假如当初还有一些其余申请占据了并发申请限度的数量,导致 home 接口申请变慢。
若是 info 接口响应慢,长时间占据这浏览器的申请过程,会导致页面首屏渲染速度更慢,那么就须要有个一套计划能够依据接口的优先级进行加载顺序控制,能够将程序变为如下。
计划: 当页面加载实现后肯定工夫后,进行低优先级接口的申请,或者触发页面的滚动、点击等时立刻进行接口申请。
此计划实用于:确定接口提早加载并不会阻塞用户的交互和操作。
将其封装为一个 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,进一步放大骨架。
六、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 中看到)。
应用 setTimeout、setInterval,兼容性相对的好,然而并未从基本解决问题。
不必 localStorage,间接上 IndexDB,然而因为代码我的项目起因,不能改变原有的太多逻辑。
综合解决方案和历史起因,只能退而求其次抉择 setTimeout 的形式解决这个问题。
读取数据
问题:
每次读取 localStorage 数据时,都须要从磁盘中读取数据,因而在解决大量数据时,可能会呈现性能问题。
计划:
能够将数据进行放到内存中缓存解决,在用户的整个操作周期内只从 localStorage 获取一次数据,须要留神的是每次对数据进行操作时,须要将 localStorage 和内存缓存的数据同步更新。
数据类型转换
问题:
在存储和读取数据时,须要将数据进行序列化和反序列化操作。这些操作可能会导致性能问题。
计划:
应用 JSON.stringify() 和 JSON.parse() 函数来解决数据的序列化和反序列化。
通过对 localStorage 存储优化当前,在红米 note 11 下面进行了简略测试,首屏关上速度晋升,对于整体晋升首屏晋升约 2%。
七、动效执行机会
页面存在渐入渐现的动效,在页面首次加载时,因为渐现动效的存在,会提早用户感知该模块,从而导致感觉页面存在更多工夫的白屏,动效如下:
外围问题是首次渲染直出 DOM 构造,不走渐现动效便可,这个比拟偏差于逻辑解决,属于体验优化的领域,主打的就是在后续有相干首屏动效时,无意识对其做一下解决,保障首屏首次渲染的残缺度。
八、渲染模块的取舍
首先看一下两种状态各自的款式:未签到 VS 已签到。
签到业务的日历会依据用户当天签到状态进行渲染,存在已签到和未签到两种渲染逻辑,因为以后的架构限度,并不能在预渲染时感用户的签到状态,导致日历局部的渲染会滞后,重大影响页面的首屏渲染速度。
第一版本优化
将签到状态进行缓存,当用户进入签到时的大抵流程如下:
当用户进入页面时,会优先获取缓存中的数据进行渲染,确保用户能够第一工夫看到日历局部的渲染,这里须要留神:1. 缓存须要联合用户 token 一起判断,防止造成切换账号时造成数据净化。2. 若是用户第一次进入或者当天未签到,会应用零碎工夫作为小日历上的数字展现,当用户批改了零碎工夫设置时,日期判断会存在误差。
缓存数据必然会先于接口响应数据,因而页面第一工夫看到的必定是缓存数据(没有缓存数据,会默认应用未签到数据)所渲染的页面,那么当接口响应实现时,须要应用实在的数据触发页面的 rerender,须要留神解决,防止造成页面闪动。
尽管这样做能够进步页面的渲染体感,当进入页面时,顶部区域还是会存在肯定工夫的空白,毕竟还是须要执行 JS 后能力执行骨架渲染逻辑,实质晋升速度为:接口响应工夫 – JS 执行工夫,在低端机体现会较为好一些,高端机体感并非太显著。
第二版优化
日历局部因为已签到和未签到的款式存在着较大的出入,不能像某些竞品一样:已签、未签的整体页面布局并未有辨别,应用一套专用的渲染逻辑,这样也导致签到业务须要将渲染日历局部的动作滞后,那么外围就是怎么解决这个问题。
综合思考后,决定将未签到款式作为预渲染时间接生成 DOM,这样能够保障用户未签到的状态下进入到页面能够第一工夫对的状态,也能够更快的实现首屏的渲染。
若是用户已签到,便在此基础之上复用今日签到的逻辑,就是会在签到实现后展现一个小的动效,将小日历变成大日历的款式。这样做的益处能够是获取到用户实在状态后,主动切换到大日历状态,成果如下。
联合用户行为剖析:少数用户一天不会屡次拜访,也就是在即不怎么就义高频率拜访用户的体验之下,进步了绝大多数用户的体验。
九、首屏数据优先申请
前置小常识:最大并发申请数
为了防止浏览器适度占用系统资源,浏览器对于同一域名下的申请数量是有肯定限度的,也就是常见的浏览器最大申请数量。
以 Chrome 浏览器举例:同一域名下,HTTP 协定最多容许同时存在 6 个 TCP 连贯进行,HTTPS 协定最多为 4 个。
业务现状
签到进入页面共计加载许多接口。
其中首屏渲染须要的几个外围接口如图红色标记所示,外围的接口滞后会导致页面数据渲染的更慢,重大影响体验,那么到底影响多少呢?能够在浏览器 Network 中查看 Waterfall。
外围接口是在其余实现后开始,是因为其没有赶上浏览器第一批次接口申请队列中,须要期待后面某些接口完结后,才会将其放到申请队列中。
动作
有了问题,接下来便是如何做:
- 首先是制定方案,如何确保接口的申请能够搭上浏览器申请队列的第一班车,实质是将之前散落在各个组件内的 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 时成果:
能够看到日期存在显著的 FOUT(无款式文本闪现)问题,设置 Swap 也是相似成果,并不合乎预期。设置属性为 Block 时成果:
能够看到第一工夫并没有渲染日期,而是有点的短暂空白,因为其能够防止 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 个左右的汉字,那么在应用字体库的时候,是不是能够默认只须要导出局部即可。
通过解决后的字体库大小如下图:
字体库数量,你得管制
下面说了一个字体库的大小是多大,就算是通过解决,起码也会有 30KB 大小,所以我的项目引入的字体品种是须要管制的,不能设计同学应用了多少品种字体设计,咱们就要照单全收。
当设计同学新增字体库时,如果字体应用在 3 次以内,是不是能够应用图片来代替文字,或者应用现有的字体库来平替。
十一、慎用三方库
业务中存在一些简略的校验、转换和动效并不需要引入三方库,尤其是因为一个较为简单的性能引入了一个较为大且冷门的库时,不仅会减少我的项目的打包体积,还会减少我的项目后续保护的沟通、学习老本。例如上面一个简略切换动效:
是一个比拟惯例的切换动效,却在我的项目中引入了一个第三方库来实现,该库的应用也是有一些学习老本,因为其具备实现比较复杂的动效能力,在业务动效具备肯定复杂度且非首屏的场景下,是能够思考引入应用的,否则相似这种首屏便须要加载的动效,还是谨慎。
上述的切换动效 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 级别。
* 文 / 来骏
本文属得物技术原创,更多精彩文章请看:得物技术官网
未经得物技术许可严禁转载,否则依法追究法律责任!