钉钉红包性能优化之路

article/2025/6/20 23:35:16

一、业务背景

请客红包、小礼物作为饿了么自研的业务产品,在钉钉的一方化入口中常驻,作为高UV、PV的toB产品,面对不同设备环境的用户,经常会偶尔得到一些用户反馈,如【页面白屏太久了】、【卡住了】等等,本文将以产品环境为出发点(App中的H5),以前端基础、加载链路、端能力三个方向进行性能优化。

基于现阶段业务架构,在应用层进行通用的性能优化action拆解。

整体优化后接近秒开,效果如下:
请添加图片描述

二、前端基础优化

2.1. 构建产物瘦身

作为前端基础优化的出水口,可通过webpack analyzer插件分析dist产物的具体分布,主要action如下:

  • 按需加载antd,减少79.28kb
  • 大型通用库接入cdn,基于externals排出构建包,减少65.08kb
  • debug工具生产环境不引入:vconsole,减少一次生产的http js请求
  • polyfill拆分,减少28.45kb
  • 钉钉、饿了么域接口返回图片裁剪,平均单张图片减少80%大小,请求时间减少80%
  • 压缩器从esbuild切换至terser(牺牲时间、提升压缩率),减少100.2kb
  • 移除无用的包:deepcopy、md5-js,减少3.58kb
  • 按需引入lodash、dingtalk-jsapi、crypto-js,减少49.18kb

关键优化代码:

// case1:按需引入大npm包
import setTitle from '@ali/dingtalk-jsapi/api/biz/navigation/setTitle';
import openLink from '@ali/dingtalk-jsapi/api/biz/util/openLink';
import setScreenKeepOn from '@ali/dingtalk-jsapi/api/biz/util/setScreenKeepOn';// case2:externals拆包,转cdn引入externals: {react: 'React','react-dom': 'ReactDOM',}// case3:图片裁剪,降低质量、大小降低图片请求耗时
import getActualSize from './getActualSize';
import getImageType from './getImageType';const getActualEleImageUrl = (url: string, size: number) => {if (typeof url === 'string' && url.includes('cube.elemecdn.com')) {const imageType = getImageType(url);const relSize = getActualSize(size);const end = `?x-oss-process=image/resize,m_mfit,w_${relSize},h_${relSize}/format,${imageType}/quality,q_90`;return `${url}${end}`;}return url;
};// case4:按需加载antd-mobileextraBabelPlugins: [['import',{libraryName: 'antd-mobile',libraryDirectory: 'es/components',},],],

结果:

  • 性能优化构建包gzip压缩后大小减少305.59KB,优化后大小474.74KB,下降39.1%;
  • 性能优化首屏加载冷启动FP减少400ms,热启动FP减少1s;

2.2. 预加载&预解析

将应用中所用到的所有请求资源的能力统一前置配置在html head,减少所有资源类请求的耗时。

  links: [{rel: 'dns-prefetch',href: 'https://g.alicdn.com/',},{rel: 'preconnect',href: 'https://g.alicdn.com/',},{rel: 'dns-prefetch',href: 'https://gw.alicdn.com/',},{rel: 'preconnect',href: 'https://gw.alicdn.com/',},{rel: 'dns-prefetch',href: 'https://img.alicdn.com/',},{rel: 'preconnect',href: 'https://img.alicdn.com/',},{rel: 'dns-prefetch',href: 'https://assets.elemecdn.com/',},{rel: 'preconnect',href: 'https://assets.elemecdn.com/',},{rel: 'dns-prefetch',href: 'https://static-legacy.dingtalk.com/',},{rel: 'preconnect',href: 'https://static-legacy.dingtalk.com/',},{rel: 'dns-prefetch',href: 'https://cube.elemecdn.com/',},{rel: 'preconnect',href: 'https://cube.elemecdn.com/',},{rel: 'preload',as: 'script',href: 'https://g.alicdn.com/??/code/lib/react/18.2.0/umd/react.production.min.js,/code/lib/react-dom/18.2.0/umd/react-dom.production.min.js',},],

2.3 分包

到的所有请求在整个项目中以页面组件、公共组件、大npm包三个方向进行分包拆解,大体分包策略是尽可能减少SPA首次访问路由的chunk体积,策略如下:

  • node_modules里面大于160kb的模块拆分成单独的chunk;
  • 公共组件至少被引入3次拆分成单独的chunk;

分包关键代码:

  optimization: {moduleIds: 'deterministic', // 确保模块id稳定chunkIds: 'named', // 确保chunk id稳定minimizer: [new TerserJSPlugin({parallel: true, // 开启多进程压缩extractComments: false,}),new CssMinimizerPlugin({minimizerOptions: {parallel: true, // 开启多进程压缩preset: ['default',{discardComments: { removeAll: true },},],},}),],splitChunks: {chunks: 'all',maxAsyncRequests: 5, // 同时最大请求数cacheGroups: {// 第三方依赖vendors: {test: /[\/]node_modules[\/]/,name(module) {const packageName = module.context.match(/[\/]node_modules[\/](.*?)([\/]|$)/)[1];return `vendor-${packageName.replace('@', '')}`;},priority: 20,},// 公共组件commons: {test: /[\/]src[\/]components[\/]/,name: 'commons',minChunks: 3, // 共享模块最少被引用次数priority: 15,reuseExistingChunk: true,},lib: {// 把node_modules里面大于160kb的模块拆分成单独的chunktest(module) {return (module.size() > 160 * 1024 &&/node_modules[/\]/.test(module.nameForCondition() || ''));},// 把剩余的包打成一个chunkname(module) {const packageNameArr = module.context.match(/[\/]node_modules[\/](.*?)([\/]|$)/);const packageName = packageNameArr ? packageNameArr[1] : '';return `chunk-lib.${packageName.replace('@', '')}`;},priority: 15,minChunks: 1,reuseExistingChunk: true,},// 默认配置default: {minChunks: 2,priority: 10,reuseExistingChunk: true,name(module, chunks) {const allChunksNames = chunks.map((item) => item.name).join('~');return `common-${allChunksNames}`;},},},},},

三、加载链路优化

3.1 DOM load前置优化

在SPA所有JS文件解析完成(整个页面呈现),在前置可增加loading态替代白屏减少用户的等待焦虑,具体的思路是在html response -> js chunk全部解析完成中间,增加一个loading状态,提升FP、FCP性能指标,具体行动是编写了一个webpack html构建完的插件,在构建结果中的html手动注入loading组件。

插件实现比较简单:

import { IApi } from 'umi';export default (api: IApi) => {// 用于在html ready到SPA应用js ready之间增加钉钉标准loadingapi.modifyHTML(($) => {$('head').prepend(`<style>body, html {width: 100%;height: 100%;margin: 0;padding: 0;border: 0;box-sizing: border-box;}#html-ding-loading-container {width: 100vw;height: 100vh;background: rgba(0, 0, 0, 0.2);opacity: 1;}#ding-loading {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);animation: lightningAnimate 1s steps(1, start) infinite;width: 3.6rem;height: 3.6rem;background-repeat: no-repeat;background-position: 0rem 0rem;background-size: 100%;background-image: url('https://img.alicdn.com/imgextra/i4/O1CN014kqXkX22y9iy5WhI6_!!6000000007188-2-tps-120-3720.png');}@keyframes lightningAnimate {0% { background-position: 0rem 0rem; }3.3% { background-position: 0rem calc(-1 * 3.6rem); }6.6% { background-position: 0rem calc(-2 * 3.6rem); }10% { background-position: 0rem calc(-3 * 3.6rem); }13.3% { background-position: 0rem calc(-4 * 3.6rem); }16.6% { background-position: 0rem calc(-5 * 3.6rem); }20% { background-position: 0rem calc(-6 * 3.6rem); }23.3% { background-position: 0rem calc(-7 * 3.6rem); }26.6% { background-position: 0rem calc(-8 * 3.6rem); }30% { background-position: 0rem calc(-9 * 3.6rem); }33.3% { background-position: 0rem calc(-10 * 3.6rem); }36.6% { background-position: 0rem calc(-11 * 3.6rem); }40% { background-position: 0rem calc(-12 * 3.6rem); }43.3% { background-position: 0rem calc(-13 * 3.6rem); }46.6% { background-position: 0rem calc(-14 * 3.6rem); }50% { background-position: 0rem calc(-15 * 3.6rem); }53.3% { background-position: 0rem calc(-16 * 3.6rem); }56.6% { background-position: 0rem calc(-17 * 3.6rem); }60% { background-position: 0rem calc(-18 * 3.6rem); }63.3% { background-position: 0rem calc(-19 * 3.6rem); }66.6% { background-position: 0rem calc(-20 * 3.6rem); }70% { background-position: 0rem calc(-21 * 3.6rem); }73.3% { background-position: 0rem calc(-22 * 3.6rem); }76.6% { background-position: 0rem calc(-23 * 3.6rem); }80% { background-position: 0rem calc(-24 * 3.6rem); }83.3% { background-position: 0rem calc(-25 * 3.6rem); }86.6% { background-position: 0rem calc(-26 * 3.6rem); }90% { background-position: 0rem calc(-27 * 3.6rem); }93.3% { background-position: 0rem calc(-28 * 3.6rem); }96.6% { background-position: 0rem calc(-29 * 3.6rem); }100% { background-position: 0rem calc(-30 * 3.6rem); }}</style>`);$('body').prepend(`<div id='html-ding-loading-container'><div id='ding-loading'></div></div>`);});
};// 在umi中注入plugins: ['@umijs/plugins/dist/initial-state','@umijs/plugins/dist/model','./plugins/loading.ts',],

实现效果:

在这里插入图片描述

3.2 session管理持久化

系统中在前端资源全部response解析完成后,对所有的业务接口请求执行前都需要确保getUserInfo接口响应成功并在前端接收sessionId,然后在所有的业务接口中携带在参数中,在系统交互链路的前置流程过长的背景下,前端基于storage实现getUserInfo数据持久化从而节省一次关键串行接口的请求。

在这里插入图片描述

关键用户信息读取的代码:

import getCurrentUserInfo$ from '@ali/dingtalk-jsapi/api/internal/user/getCurrentUserInfo';export async function fetchUserInfo() {const dingUid = await getCurrentUserInfo();let storageUserInfo = getUser();let res: UserDto;if (storageUserInfo && +dingUid?.uid === +storageUserInfo?.dingUserId) {// 如果缓存中的用户信息是当前用户,使用缓存res = {userType: 'dingtalkUid',userId: storageUserInfo.dingUserId,userName: storageUserInfo.nick,name: storageUserInfo.nick,mobile: storageUserInfo.mobile,avatarUrl: storageUserInfo.avatarUrl,};window.enjoyDrinkTrace.logError('命中storageUserInfo缓存');} else {// 未命中缓存,走请求用户信息流程const corpId = getQueryString('corpId');const userInfo = await getUserInfo({corpId,userChannel: getQueryString('__from__') || '',});res = {userType: 'dingtalkUid',userId: userInfo.data.data?.userID,userName: userInfo.data.data?.userName,name: userInfo.data.data?.userName,mobile: userInfo.data.data?.mobile,avatarUrl: userInfo.data.data?.avatarUrl,};}return res;
}

这一步优化在FP节点之后,减少了与FCP中间的耗时,减少量为一次接口请求的时间,约100ms。

四、做好基本的,再借助一下端能力

4.1 离线策略

做好前端基本的优化+H5加载链路的优化后结合cdn自带的缓存,整体的首屏用户体验已经很不错了。

那如native般的秒开,怎么实现?由于业务运行在钉钉中,咨询了钉钉同学,对于产品首页、红包页等页面布局不大的场景中尝试接入离线。

结合实际业务场景,在请客红包中,所有资源都可根据离线预置到App本地,在用户访问页面时,可以尽早为页面渲染铺垫;此外还可以推送相应的 js 缓存文件,减少 js 下载时长,让用户可交互时间提前;页面中的固定图片,也可以通过 zcache 缓存,提升页面图片整体的缓存命中率。

结合了所有的优化后,请客红包的IM消息主入口基本做到秒开。

请添加图片描述

五、未来规划

结合各类性能优化的手段,沉淀出相对应的代码、文档、prompt、tools等,集成到agent中,在未来的相关新产品设计中,让业务在起步阶段就有相对应稳定、体验较好的体感。

基于ARMS性能插件、钉钉容器性能监控看板,持续提升业务性能,保障业务用户体验。

对于场景投放类页面(目前是MPA多页方案),后续考虑转SSR


http://www.hkcw.cn/article/ywNYARsvQI.shtml

相关文章

鲲鹏Arm+麒麟V10 K8s 离线部署教程

针对鲲鹏 CPU 麒麟 V10 的离线环境&#xff0c;手把手教你从环境准备到应用上线&#xff0c;所有依赖包提前打包好&#xff0c;步骤写成傻瓜式操作指南。 一、环境规划# 准备至少两台机器。 架构OS作用Arm64任意&#xff0c;Mac 也可以下载离线包Arm64麒麟 V10单机部署 K8s…

Redis主从复制详解

概述 Redis 的主从复制&#xff08;Master-Slave Replication&#xff09;是实现数据备份、读写分离和水平扩展的核心机制之一。通过主从复制&#xff0c;一个主节点&#xff08;Master&#xff09;可以将数据同步到多个从节点&#xff08;Slave&#xff09;&#xff0c;从节点…

16.进程间通信(二)

一、命名管道 1.概念 匿名管道解决了具有血缘关系的进程之间的通信&#xff0c;如果两个进程毫不相干&#xff0c;如何进行通信呢&#xff1f;通过文件&#xff0c;管道文件。 对于两个不同进程&#xff0c;打开同一路径下的同一文件&#xff0c;inode和文件内核缓冲区不会加载…

优化的两极:凸优化与非凸优化的理论、应用与挑战

在机器学习、工程设计、经济决策等众多领域&#xff0c;优化问题无处不在。而在优化理论的世界里&#xff0c;凸优化与非凸优化如同两个截然不同的 “王国”&#xff0c;各自有着独特的规则、挑战和应用场景。今天&#xff0c;就让我们深入探索这两个优化领域的核心差异、算法特…

day15 leetcode-hot100-29(链表8)

19. 删除链表的倒数第 N 个结点 - 力扣&#xff08;LeetCode&#xff09; 1.暴力法 思路 &#xff08;1&#xff09;先获取链表的长度L &#xff08;2&#xff09;然后再次遍历链表到L-n的位置&#xff0c;直接让该指针的节点指向下下一个即可。 2.哈希表 思路 &#xff0…

rtpinsertsound:语音注入攻击!全参数详细教程!Kali Linux教程!

简介 2006年8月至9月期间&#xff0c;我们创建了一个用于将音频插入指定音频&#xff08;即RTP&#xff09;流的工具。该工具名为rtpinsertsound。 该工具已在Linux Red Hat Fedora Core 4平台&#xff08;奔腾IV&#xff0c;2.5 GHz&#xff09;上进行了测试&#xff0c;但预…

谷歌Stitch:AI赋能UI设计,免费高效新利器

在AI技术日新月异的今天&#xff0c;各大科技巨头都在不断刷新我们对智能工具的认知。最近&#xff0c;谷歌在其年度I/O开发者大会期间&#xff0c;除了那些聚光灯下的重磅发布&#xff0c;还悄然上线了一款令人惊喜的AI工具——Stitch。这是一款全新的、完全免费的AI驱动UI&am…

PowerBI企业运营分析——线性回归销售预测

PowerBI企业运营分析——线性回归销售预测 欢迎来到Powerbi小课堂&#xff0c;在竞争激烈的市场环境中&#xff0c;企业运营分析平台成为提升竞争力的核心工具。 该平台通过整合多源数据&#xff0c;实现关键指标的实时监控&#xff0c;从而迅速洞察业务动态&#xff0c;精准…

<4>, Qt窗口

目录 一&#xff0c;菜单栏 二&#xff0c;工具栏 三&#xff0c;状态栏 四&#xff0c;浮动窗口 五&#xff0c;对话框 一&#xff0c;菜单栏 MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) {ui->setupUi(this);// 创建菜单栏…

多目标粒子群优化算法(MOPSO),用于解决无人机三维路径规划问题,Matlab代码实现

多目标粒子群优化算法&#xff08;MOPSO&#xff09;&#xff0c;用于解决无人机三维路径规划问题&#xff0c;Matlab代码实现 目录 多目标粒子群优化算法&#xff08;MOPSO&#xff09;&#xff0c;用于解决无人机三维路径规划问题&#xff0c;Matlab代码实现效果一览基本介绍…

具有离散序列建模的统一多模态大语言模型【AnyGPT】

第1章 Instruction 在人工智能领域、多模态只语言模型的发展正迎来新的篇章。传统的大型语言模型(LLM)在理解和生成人类语言方面展现出了卓越的能力&#xff0c;但这些能力通常局限于 文本处理。然而&#xff0c;现实世界是一个本质上多模态的环境&#xff0c;生物体通过视觉、…

嵌入式学习笔记 - STM32 HAL库以及标准库内核以及外设头文件区别问题

一 CMSIS内核驱动文件夹 标准库中CMSIS内核驱动文件夹中&#xff0c;仅包含两个.h文件&#xff0c;其中stm32f10x.h 为stm10系列底层文件如总线以及各片上外设模块寄存器地址&#xff0c;system_stm32f10x.h为系统底层配置文件&#xff0c;主要为时钟配置。 HAL库中CMSIS内核驱…

LeetCode 算 法 实 战 - - - 移 除 链 表 元 素、反 转 链 表

LeetCode 算 法 实 战 - - - 移 除 链 表 元 素、反 转 链 表 第 一 题 - - - 移 除 链 表 元 素方 法 一 - - - 原 地 删 除方 法 二 - - - 双 指 针方 法 三 - - - 尾 插 第 二 题 - - - 反 转 链 表方 法 一 - - - 迭 代方 法 二 - - - 采 用 头 插 创 建 新 链 表 总 结 &a…

Ros真(node?package?)

Ros中 都是靠一个个节点相互配合的 如同APP之间的配合 然后节点不好单独存在&#xff0c; 我们一般把他们放在一个包里 也就是Package。 也可以自己设立一个包 如图这种 ———————————— 建立包 流程 &#xff1a; —————— 我们弄好之后 在VSCODE SRC右键 …

电路图识图基础知识-常用仪表识图及接线(九)

一、 直流电流表的使用和接线 用来测量直流电流的仪表&#xff0c;我们称为直流电流表&#xff0c;下图所示为直流电流表。 直流电流表有两种接入方式&#xff1a;直接接入法、间接接入法。下图所示为直流电流表接线方 法 。 4.1.2 交流电流表的使用和接线 交流电流表也是一种…

分享两款使用免费软件,dll修复工具及DirectX修复工具

装软件老是弹窗报错&#xff1f;两个小工具解决系统运行库问题 安装软件时弹出DLL缺失&#xff1f;别急&#xff0c;这里有办法 安装软件的时候&#xff0c;突然跳出个弹窗&#xff0c;提示缺少什么“MSVCP140.dll”或者“VCRUNTIME140.dll”&#xff0c;完全不懂。这种情况并…

L56.【LeetCode题解】 电话号码的字母组合

目录 1.17. 电话号码的字母组合 2.分析 举例 枚举算法:使用递归(dfs) 递推 回归 特殊情况的考虑 代码 提交结果 事后回顾: 递归调用的部分展开图 1.17. 电话号码的字母组合 https://leetcode.cn/problems/letter-combinations-of-a-phone-number/ 给定一个仅包含数字…

基础补充(扩展方法/协变/访问修饰/接口)

文章目录 项目地址一、扩展方法&#xff08;Extension Methods&#xff09;1.1 创建扩展方法1.2 案例 二、访问修饰符2.1 顶级类2.2 类中成员&#xff08;字段、属性、方法&#xff09; 项目地址 教程作者&#xff1a;教程地址&#xff1a; 代码仓库地址&#xff1a; 所用到的…

MySQL 事务解析

1. 事务简介 事务&#xff08;Transaction&#xff09; 是一组操作的集合&#xff0c;它是一个不可分割的工作单位&#xff0c;事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 经典案例&#xff1…

【Qt】Bug:findChildren找不到控件

使用正确的父对象调用 findChildren&#xff1a;不要在布局对象上调用 findChildren&#xff0c;而应该在布局所在的窗口或控件上调用。