商智C店H5性能优化实战

前言

商智C店,是依托移动低码能力搭建的一个应用,产品面向B端商家。随着应用体量持续增大,考虑产品定位及用户体验,我们针对性能较差页面做了一次优化,并取得了不错的效果,用户体验值(UEI)从一般提升到良好。本文详细记录了优化思路及过程,期望给正在或打算做用户体验提升的小伙伴提供一些参考。

一、性能优化概览

从宏观方面讲,前端性能优化主要包含两个层面:

•快速的首屏响应

指的是从用户输入URL到页面完整渲染出来的过程中,通过减少资源请求时长、合理化页面渲染等方式,让页面内容尽快呈现到用户面前,达到舒适的展示效果。

•流畅的交互体验

指的是在资源加载且页面渲染完成后,用户与页面的交互反馈及时且动效流畅,没有卡顿、掉帧等情况。

本文重点介绍前者,性能监测工具依赖烛龙平台(性能指标来自LightHouse性能工具)。

二、性能分析

1、Lighthouse工具

Lighthouse 是 Google Chrome 推出的一款开源自动化工具,它可以搜集多个现代网页性能指标,分析 Web 应用的性能并生成报告,为开发人员进行性能优化提供参考方向。

1.1 性能指标

•FCP

首次内容绘制(First Contentful Paint),是指测量在用户导航到页面后浏览器呈现第一段 DOM 内容所花费的时间。DOM 内容包括页面上的图像、非白色元素和 SVG 等,不包括 iframe 内的任何内容。

•LCP

最大内容绘制(Largest Contentful Paint):测量视口中最大的内容元素何时呈现到屏幕上,这近似于页面的主要内容对用户可见的时间。

•SI

速度指数(Speed Index),反映了网页内容填充的速度。页面解析渲染过程中,资源的加载和主线程执行的任务会影响到速度指数的结果。

•CLS

累积布局偏移(Cumulative Layout Shift),主要测量可见元素在可视区域内的移动情况。该指标需要重点关注,随着Lighthouse的版本迭代,该指标占比一直呈上升趋势,也是页面性能优化容易忽略的方面。以下为Lighthouse不同版本中CLS指标的占比情况:

•TBT

总阻塞时间(Total Blocking Time),是指测量页面被阻止响应用户输入(例如鼠标点击、屏幕点击或键盘按下)的总时间。它是通过添加 FCP 和 TTI 之间所有长任务的阻塞部分来计算总和,任何执行时间超过 50 毫秒的任务都是长任务。

各指标表现评价标准:

1.2 UEI计算规则

UEI指的是用户体验值,烛龙平台底层使用的是Lighthouse性能统计工具。其中,单页面的得分统计规则与其保持一致(性能指标分数的加权平均值)。针对多页面应用,整体评分计算规则为,每个页面得分的加权平均值:(页面A * A访问量 + 页面B * B访问量 …)/ (A访问量 + B访问量 + …)。

2、Chrome调试工具

Chrome的Performance面板,可以记录页面加载时\运行时的所有活动,方便我们找出页面的性能瓶颈。

以上为performance面板的概览图,通过对页面加载过程的录制,查看视图面板的主线程工作过程,我们能够找到页面阻塞的长任务,从而优化TBT指标;交互阶段存在的动画卡顿等问题,也可以通过该面板进行分析和优化。

三、实战

1、项目介绍

商智C店,是依托移动低码搭建的面向B端商家的应用。根据烛龙数据统计,优化之前,用户体验值在58分左右。我们期待通过本次优化,将烛龙统计得分提升到体验良好的水平(75分及以上)。从下图可以看出,页面的CLS、TBT分数很低,LCP指标分数位于性能良好与性能较差之间,这三个指标是我们接下来进行优化的重点。

2、过程记录

2.1 减小资源体积

2.1.1 抽取公共依赖

商智C店采用了主应用加三个微应用的架构,这三个微应用的包大小为:

可以发现,每个包的体积都很大(5000k以上),这势必会影响资源下载速度并拖慢代码执行效率。微应用包体积过大的原因是:主应用和多个微应用间存在重复打包,如组件库(自研)、公共库依赖(react、react-dom、echarts)等内容。

针对该问题,可在制品的生成阶段做处理:生成两个包,一为完整的包,保证其能够独立使用;二为优化的包,里面剔除了公共依赖及组件库的样式。调整后,包的体积减少1300多k,总包大小减少至4000k左右。

需要说明的是,自研组件库目前并没有提供UMD格式打包方式,因此以上提到的公共依赖不包含自研组件库相关的包(后续会处理)。

2.1.2 tree shaking优化

tree shaking(摇树) ,用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。最初起源于 ES2015 模块打包工具 rollup。

自研组件库主要包含三个包,PC组件库jmtd、移动组件库jmtm、jmtd-hooks库。其中hooks库是对两端组件库的二次封装,帮助低码平台处理组件间的逻辑编排功能,三个库均通过npm包的方式引入。

排除公共依赖及样式后,微应用制品的体积大约在4000k左右,这显然还不是一个理想值。打包构建工具依赖webpack5,根据webpack性能分析插件分析,定位到jmtd-hooks包的体积过大。按照固有思维,webpack5在生产环境默认支持tree shaking,因此生成的制品只会打包使用到的代码,并不会引起制品的体积过大。那么问题出在哪里呢?

原因在于,jmtd-hooks库的打包方式有问题。因hooks包较强的业务属性,我们将原始文件打包成了一个文件。

webpack的tree shaking有两种方式:

•usedExports:开启该配置后,能够在打包环节找到未被使用且无副作用的函数且不会对其打包。 它只针对单文件生效,不能跨文件进行分析。而js是一门动态语言,因其灵活多变的特性,编译工具很难准确分析某个函数是否具有副作用,因此其优化能力较弱。

•sideEffects:具有跨文件分析能力,如果某个库的package.json中配置了sideEffects为true,表明该库中的所有文件都没有副作用。若某文件中的方法未被使用,编译工具可放心的进行摇树,其优化能力更强。

分析后可得出结论:webpack对单文件,仅利用其静态分析能力摇树效果很弱,导致微应用制品包含了大量未使用的代码。解决办法也很简单,调整jmtd-hooks的打包方式,保留原始的文件结构。调整后hooks包的结构为:

可看到,制品体积有了大幅的减小,最终控制在2000k以下。经过抽取公共依赖和摇树优化两个步骤,整体包体积减少**65%**左右。

2.1.3 删除项目冗余代码

随着需求持续迭代,项目中很容易存在一些无效的脚本及样式引用,或者一些不规范的写法,如未使用的全局变量、没有意义的console等,都需要进行删除,保持代码的精简性。

完成以上步骤后,我们进行了一次上线,并连续监测了一周的数据。用户体验值有了明显的提升,从以前的58分提升到65分

优化前:

优化后:

以下为优化前后各指标的对比数据(单位:ms):

2.2 优化资源加载速度

一个页面从输入URL到最后在浏览器的渲染流程大致如下:

URL解析 => DNS解析 => 建立TCP连接 => 客户端发送请求 => 服务器处理和响应请求 => 浏览器解析并渲染响应内容 => 断开TCP链接

从URL解析到服务器响应请求的过程属于网络层面,常用的优化手段包括:

1、缓存技术:合理的使用缓存,如CDN缓存、浏览器缓存等,来加快资源的下载速度。

2、文件压缩合并:通过压缩工具减少资源大小,多个文件合并成一个文件,减少网络请求开销。

3、开启gzip压缩:在文件传输阶段,开启gzip压缩,减小数据传输量,节省服务器网络带宽。

4、http2协议:该协议带来了很多新的特性,如二进制分帧、头部压缩等,把证文件传输更加安全高效。

5、DNS预解析:初次请求某个跨域域名,需先解析该域名,其解析时间很容易被忽视,DNS预解析能够减少用户等待的时间。

6、域名分片:域名分片指的是将同一站点下的静态资源分布在不同域名下,其最大的好处是,能够突破浏览器下载资源的并发限制。

7、代码分割:将整个应用代码合理分割成多个部分,能够有效减小首屏资源的请求体积,提升资源下载速度。

2.2.1 代码分割

合理的分割代码,能够有效加快首屏资源请求速度。出于此考虑,我们将项目结构分为了一个主应用加三个微应用的模式,主应用作为基座来处理导航及微应用间通信,三个微应用(经营分析、行业竞争、我的关注)作为独立模块,承接细分的业务功能。以模块划分代码,大体上解决了问题,但针对某些高频使用的详情页,还有进一步优化的空间。

根据埋点数据统计,用户高频使用的主要有两个页面:经营分析页和指标详情页,其访问的PV值占到了90%以上。经营分析页的表现尚可,用户体验值在70分左右,可暂不优化;指标详情页的表现堪称灾难,仅得到58分,明显低于现阶段65分的平均值。

分析原因:指标详情页为一个二级路由页面,可通过核心指标页下钻到该页,也可通过客户端首页直接查看某个指标的详情页。这将导致,在查看该页面前,需先加载经营分析模块。

解决思路:将指标详情页独立为一个微应用,单独维护。当用户通过京麦客户端直接访问某个指标的详情时(用户使用此场景频率很高),只加载该微应用代码,避免一级路由页代码的影响。

优化后效果很明显,指标详情页得分从58分直接飙升到80分,各个维度的指标都有了不错的提升。

2.2.2 资源预加载

资源预加载是性能优化的常用手段,针对模块较多的应用尤其有效。前文我们已将商智C店分割为主应用加三个微应用的代码结构,首页加载的是主应用加经营分析模块,当用户切换其它的模块(竞争分析、我的关注等),需要先下载该模块的制品(2000k左右),再进行内容的渲染。

采用资源预加载方案,可在首页内容加载完成后,利用浏览器的空余资源,预加载其它模块的文件,当用户切换到其它模块时,资源已经提前加载完毕,省去资源下载的时间。

具体实现逻辑为:

// 通过动态创建script的方式,预加载文件
// 脚本加载并解析完成后,会将执行逻辑挂载到全局变量上
function loadScript (url) {
    if (!cache || cache[url]) return;
    cache[url] = new Promise(resolve => {
        const script = document.createElement('script');
        script.src = url;
        script.onload = () => {
            document.body.removeChild(script);
            resolve(window.__microApp__);
        };
        document.body.appendChild(script);
    });
}

// 当页面加载完成后,通过requestIdleCallback方法,利用浏览器空余资源来预加载文件
window.onload = function () {
    requestIdleCallback(() => {
        microAppArr.forEach(({ src }) => loadScript(src));
    });
};


资源预加载之前,行业竞争模块的得分在60分,优化之后,提升到了68分

优化之前,我的关注模块的得分在65分,优化之后提升到72分

2.2.3 DNS预解析

当你的网站第一次请求某个跨域域名时,需要先解析该域名(例如页面访问cdn资源,第一次访问需要先解析cdn),而在该请求之后的请求都没有这项时间支出。典型的一次DNS解析需耗费20-120毫秒,其解析的时间很容易被忽视。DNS Prefetching 是具有此属性的域名,不需要用户点击链接就在后台解析,这个方式能减少用户的等待时间,提升用户体验。

<!-- 用meta信息来告知浏览器, 当前页面要做DNS预解析 -->
<meta http-equiv="x-dns-prefetch-control" content="on" />
<!-- 在页面header中使用link标签来强制对DNS预解析 -->
<link rel="dns-prefetch" href="xxx.com"> 


商智C店具体应用案例:

2.2.4 域名分片

域名分片指的是将同一站点下的静态资源分布在不同域名下。例如:主站域名 www.a.com;图片域名 www.a-img.com;脚本、样式等文件的域名 www.a-link.com。其最大的好处是,能够突破浏览器下载资源的并发限制,具体可参考文章。

同样在商智C店中,我们也应用了该思路,如将React、Echarts等公共库、业务打包代码、图片等静态资源分别放在不同的CDN域名,从而突破浏览器的并发请求限制。

2.2.4 雪碧图

主应用的底部是一个菜单导航,它的图标包含了6张图片(选中和非选中态使用不同图片),一个合理的方式是,使用雪碧图(Sprite),将多个连续的图片合并为一张,通过改变背景图位置来控制图片显示。

2.3 优化用户感知

2.3.1 滚动加载

微应用模块的展示形式为橱窗卡片组成的长列表(10个左右,后续需求迭代还会增加)。橱窗卡片作为基础的布局组件,承接了筛选区域、下钻功能、可视化组件的展示功能,如果在首屏一次加载太多,将会导致较大的渲染压力。

滚动加载是提升首屏渲染性能的很好方案,它的核心是判断目标元素(橱窗卡片)是否在可视区域范围内,当其不可见时,只需在卡片内容区渲染一个有默认高度的占位元素,同时也不必加载筛选区域组件,当用户滚动到该区域时,再渲染业务相关的内容。

如图所示,首屏只需渲染核心指标和目标监控两个橱窗卡片的内容,其余卡片只渲染一个占位元素,有效缓解浏览器的渲染压力。那么具体逻辑是怎么实现的呢?

通常有两种方式可判断元素是否在可视范围内:

1、监听scroll事件,获取目标元素(橱窗卡片)相对于视口的坐标,再判断其是否在可视区域内。该方法的缺点是,scroll事件密集触发,容易造成性能问题。

2、IntersectionObserver API,翻译为”交叉观察器“,可以自动"观察"元素是否可见,Chrome 51+ 支持。

这里我们采用第二种方案,初始状态设置内容区域不可见,当元素挂载完成后,创建io监听器,监听到卡片位于可视范围内,再加载对应组件,具体逻辑参考以下代码。

function WindowCard ({ title, filter, children }) {
    const wcRef = useRef(null);
    // 初始状态,默认设置内容区域不可见
    const [visible, setVisible] = useState(!useObserver);
    useEffect(() => {
        // 元素挂载后,创建io监听器
        const io = new IntersectionObserver(entries => {
            if (entries[0].intersectionRatio <= 0) return;
            // 当元素位于可视区域范围内后,加载业务组件
            setVisible(true);
            // 业务组件加载后,断开监听
            io.unobserve(wcRef.current);
        });
        io.observe(wcRef.current);

        return () => {
            io.disconnect();
        };
    }, []);

    return (
        <div className='window-card' ref={wcRef}>
            // 省略head部分逻辑
            <div className='window-card-content'>
                {visible ? children : <div style={{ height: 200 }} />}
            </div>
        </div>
    );
}


2.3.2 优化CLS指标

根据LightHouse统计数据,CLS指标得分值很低,其反映了在首屏页面加载时,出现了较大的布局偏移。

类似于核心指标卡片,在指标卡加载前,内容区域高度为0,而loading加载后,有一个200px的占位高度,真实的指标卡渲染出来后,高度变为322px,用户能够感觉到明显的抖动。

还有指标详情页中的场景,在指标卡loading阶段,占位高度为200px,而真实渲染的指标卡的高度为142px。假如用户在此时点击某个可点击区域,很容以造成误触,带来很差的用户体验。

诸如以上的场景,我们事先能够确定指标卡渲染出来的真实高度,可以提前将其固定。参照类似的方法,检查其他的页面,能够尽量避免页面首屏的抖动。

3、实战总结

通过抽取公共依赖和摇树优化,微应用的体积从5000多k减小到2000k以内,应用体验值从58分提升到65分。通过对长列表页面适配滚动加载,提升了页面的渲染效率;固定首屏指标卡等元素的高度,减少了页面布局偏移,从而保证页面稳定的显示效果;将用户高频使用页面(指标详情页)独立为微应用,直接将拖后腿的页面变成性能表现优秀的页面;资源预加载,充分利用了浏览器的空余资源,有效提高页面加载速度。应用的体验值,从65分提升到75分:

优化前:

优化后:

细分指标的表现都有了明显提升,以下为优化前后的对比数据:

本次优化的重点是提升商智C店首屏显示的效果,目标是将烛龙统计得分提升到体验良好的水平(75分及以上),基本完成了预定目标。因优化过程位于需求迭代的间隙,并不计划对系统架构及业务代码进行深度调整,因此还有些遗留工作放到了后续:如微应用的体积仍有进一步优化的空间、TBT指标得分虽有提升,但表现仍较差、某些组件交互动效卡顿等。

最后,期望本文的经验能给同样在做性能优化的小伙伴一些参考,文章的不足之处,可在评论区讨论。敬请期待后续移动端交互优化的文章。

作者:京东零售 郄鹏飞

来源:京东云开发者社区 转载请注明来源

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

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

相关文章

每日一题——LeetCode1089.复写0

方法一 splice&#xff1a; 通过数组的slice方法&#xff0c;碰到 0就在后面加一个0&#xff0c;最后截取原数组的长度&#xff0c;舍弃后面部分。 但这样做是违反了题目的要求&#xff0c;不要在超过该数组长度的位置写入元素。 var duplicateZeros function(arr) {var le…

docker 完成MySQL的主从复制

文章目录 搭建步骤 搭建步骤 拉取镜像 docker pull mysql:5.7运行主从 docker run -p 3307:3306 --name mysql-master -v /mydata/mysql-master/log:/var/log/mysql -v /mydata/mysql-master/data:/var/lib/mysql -v /mydata/mysql-master/conf:/etc/mysql -e MYSQL_ROOT_P…

Springboot进行多环境配置的2种方式

本文来说下Springboot使用Spring Profile和Maven Profile进行多环境配置 文章目录 概述Spring Profile多环境主配置文件与不同环境的配置文件 Maven ProfileProfile配置资源过滤 Spring Profile与Maven Profile具体使用 概述 原因 在实际的项目上&#xff0c;一般会分三种环境d…

淘宝商品详情API接口(item_get-获得淘宝商品详情)主图,属性,sku,价格,搜索商品列表

淘宝开放平台提供了API接口&#xff0c;允许开发者获取淘宝商品的相关信息。为了获取商品详情&#xff0c;您可以使用 item_get API接口。以下是如何使用此API接口来获取商品的主图、属性、SKU、价格以及搜索商品列表的简要说明&#xff1a; 公共参数 名称类型必须描述keyStr…

如何利用MiniTab的命令行来提高数据建模效率

使用MiniTab进行数据建模时&#xff0c;如果涉及到需要多次更改数据、多次查看模型&#xff0c;感兴趣的同学可以尝试一下&#xff0c;把命令行显示出来&#xff0c;通过命令行的形式来执行&#xff0c;避免在繁多的菜单中到处查找。 操作方式如下图&#xff1a; 点击菜单“查…

Transformer架构和对照代码详解

1、英文架构图 下面图中展示了Transformer的英文架构&#xff0c;英文架构中的模块名称和具体代码一一对应&#xff0c;方便大家对照代码、理解和使用。 2、编码器 2.1 编码器介绍 从宏观⻆度来看&#xff0c;Transformer的编码器是由多个相同的层叠加⽽ 成的&#xff0c;每个…

Java重修第三天—“方法“的案例练习

案例一&#xff1a;买飞机票 题目 用户购买机票时&#xff0c;机票原价会按照淡季、旺季&#xff0c;头等舱还是经济舱的情况进行相应的优惠&#xff0c;优惠方案如下:5-10月为旺季&#xff0c;头等舱9折&#xff0c;经济舱8.5折。11月到来年4月为淡季&#xff0c;头等舱7折&…

内核线程创建-kthread_create

文章参考Linux内核线程kernel thread详解 - 知乎 大概意思就是早期创建内核线程&#xff0c;是交由内核处理&#xff0c;由内核自己完成&#xff08;感觉好像也不太对呢&#xff09;&#xff0c;创建一个内核线程比较麻烦&#xff0c;会导致内核阻塞。因此就诞生了工作队列以及…

美格智能5G RedCap模组SRM813Q通过广东联通5G创新实验室测试认证

近日&#xff0c;美格智能5G RedCap轻量化模组SRM813Q正式通过广东联通5G创新实验室端到端的测试验收&#xff0c;获颁测评证书。美格智能已连续通过业内两家权威实验室的测试认证&#xff0c;充分验证SRM813Q系列模组已经具备了成熟的商用能力&#xff0c;将为智慧工业、安防监…

docker - 常用容器部署命令大全(MySQL、Redis、RabbitMQ、ES、Kibana、Nacos、Sentinel)

目录 一、常用容器运行指令 MySQL Redis RabbitMQ ElasticSearch & kibana Nacos Sentinel 一、常用容器运行指令 MySQL docker run -d --name mysql -p 3306:3306 -e TZAsia/Shanghai -e MYSQL_ROOT_PASSWORD1111 mysql:5.7 -e TZAsia/Shanghai&#xff1a;指定…

听GPT 讲Rust源代码--compiler(26)

File: rust/compiler/rustc_target/src/abi/call/mips.rs 在Rust源代码中的rust/compiler/rustc_target/src/abi/call/mips.rs文件是关于MIPS架构的函数调用ABI(Aplication Binary Interface)定义。ABI是编程语言与底层平台之间的接口规范&#xff0c;用于定义函数调用、参数传…

centos7部署minio单机版

一、目标 在centos7上部署minio单机版 二、centos7部署minio 1、下载minio mkdir /usr/local/minio cd /usr/local/minio wget https://dl.minio.io/server/minio/release/linux-amd64/minio chmod x minio 2、新建minio存储数据的目录 mkdir -p /data/minio/data3、新建…

首次引入大模型!Bert-vits2-Extra中文特化版40秒素材复刻巫师3叶奈法

Bert-vits2项目又更新了&#xff0c;更新了一个新的分支&#xff1a;中文特化&#xff0c;所谓中文特化&#xff0c;即针对中文音色的特殊优化版本&#xff0c;纯中文底模效果百尺竿头更进一步&#xff0c;同时首次引入了大模型&#xff0c;使用国产IDEA-CCNL/Erlangshen-Megat…

外包干了1个月,技术退步一大半。。。

先说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入广州某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…

FFmpeg读取Assets资源文件

在Android开发中我们经常把原生资源文件放在assets目录下以供需要时读取&#xff0c;通过API提供的resources.assets.open(filename)/openFd(filenam)方法可以非常方便获得InputStream或FileDescriptor&#xff08;文件标识符&#xff09;&#xff0c;但是在使用FFmpeg读取Asse…

Keil使用手册

文章目录 1 设置1.1 背景1.2 Project窗口显示.h文件1.3 注释1.4 Project窗口消失TAB转空格的设置keilsourceInsight 显示cannot evaluate普通局部变量静态全局变量静态局部变量 2 报错与解决2.1 warning&#xff1a;#1-D last line of file ends without anewline2.2 中文乱码 …

13. 强化学习编程实验1-在格子世界中寻宝(1)

文章目录 1.实验目的2.任务描述3.任务分析3.1 待求问题是多步决策问题否3.2 问题求解过程是一个马尔科夫决策过程3.3 状态空间S的确定3.4 动作空间A的确定3.5 状态转移概率P的确定3.6 立即回报R的确定3.7 折扣 γ \gamma γ的确定 4. 编程架构4.1 程序中有哪些对象和类4.2 环境…

Python中的@abstractmethod

abstractmethod 是 Python 中 abc 模块&#xff08;Abstract Base Classes&#xff09;提供的一个装饰器&#xff0c;用于声明抽象方法。抽象方法是指在抽象类中声明但没有提供具体实现的方法&#xff0c;而是由其子类提供具体实现。 使用 abstractmethod 装饰器可以使得子类在…

ES -极客学习

Elasticsearch 简介及其发展历史 起源 Lucene 于 Java 语言开发的搜索引擎库类创建于 1999 年&#xff0c;2005 年成为 Apache 顶级开源项目Lucene 具有高性能、易扩展的优点Lucene 的局限性 只能基于 Java 语言开发类库的接口学习曲线陡峭原生并不支持水平扩展原生并不支持水…