【大前端攻城狮之路】百度爱番番线索列表性能优化方案
导读:「以客户为中心,技术为产品服务」是百度爱番番线索管家团队一贯遵循的原则。技术架构规划首先应该围绕业务诉求展开,用合理的技术赋能产品,产品在不断的演进中又对技术提出更高的标准和要求。作为爱番番PV最高的页面,本文将详细介绍线索列表如何从快速交付的刀耕火种原始状态,逐步走向“高可用、高质量、高体验“的成熟期。
全文9355字,预计阅读时间24分钟。
在后台系统中,列表是最常见的数据展示方式之一,它就像系统的水电煤一样平常,以至于你可能会问列表开发能有什么技术挑战呢?
一、列表应该提供什么能力?
列表页有三个基本模块:
- 搜索
- 搜索框(定向查找某条数据)
- 筛选项(预置搜索条件快速找到符合条件的结果)
- 数据呈现
- 表头
- 数据
- 分页器
- 操作项
- 操作按钮(对整行数据),比如线索列表的分配、打电话
- 修改数据(对某个字段),比如添加标签
列表承载着业务上的数据,列表的设计体验关乎用户对业务数据的处理和管理效率,最终的目标是为了能提高客户使用数据及决策的效率。
二、技术落地演进
随着爱番番产品的快速迭代与演进,线索列表也从快速交付的刀耕火种原始状态,逐步走向“高可用、高质量、高体验”的成熟期。下面将逐一介绍线索列表经历过的不同版本,踩过的坑以及相应的解决方案。
V1 - 快速落地基础能力
爱番番项目初期,为保证项目快速落地,帮助用户及时快速查询和跟进线索,线索信息存放到MySQL,提供基础的CRUD能力。
V2 - 配置扩展能力
随着业务不断演进,基础的表单及列表功能与用户可配置、可扩展需求之间的矛盾愈发突出,如何支持自定义能力,是线索列表面临的一大难题。爱番番线索管家团队主要从「元数据驱动」,「指标解析器」,「前端渲染引擎」三方面着手解决。2.1 元数据驱动
伴随线索产品不断迭代,线索预置字段数量不断增加。并且线索升级能力支持自定义字段,原来页面固定的检索项已不能满足用户对所有线索字段进行检索的需求,所以我们提出了通过自定义检索项元数据动态完成检索项的渲染,以满足用户的个人偏好。
1、UI渲染信息完成检索项在页面动态渲染2、检索条件构建器,动态构建检索项指标数据3、检索项操作符支持不同指标的检索范围4、检索项关系符可以动态组装检索条件的查询场景5、当有新的筛选指标需要添加时,只需要添加检索项元数据和实现检索项的构建器
2.2 指标解析器
随着线索字段的不断增加, 在列表中需要实现表头的灵活配置,和列表项的快速加载, 提炼了自定义列表指标项元数据。
1、 自定义列表在列表检索的这个服务中,通过接入通用自定义指标服务来完成用户自定义列表项的灵活配置。2、 指标检索配置化通过引入检索指标的概念,可以将列表字段的查询抽象为对指标的查询。这样的话不同的查询场景即可建模为对不同指标数据的查询,指标数据之间可以任意组合以满足需求。此时底层只需要提供一个通用的指标检索服务即可。3、 后置处理器对于不同的场景,列表字段需要不同的展现形式;通过引入后置处理器,可以通过配置化的方式定制渲染逻辑。新指标只需在配置对应的指标元数据即可。
2.3 前端配置化引擎设计
最初线索列表和筛选项仅支持较少字段展示和筛查,因此我们只需要对特定字段进行特殊处理UI组件进行渲染即可。但随着业务复杂度增加、字段增多以及自定义字段的加入,单纯针对特定字段进行特殊处理进行渲染带来了巨大维护成本(增加字段需要为此字段单独开发UI组件,需不断调整相关业务代码)和测试回归成本,且无法满足自定义字段的筛选及展示。于是引入配置化引擎设计,以组件为基本单位,通过组件参数配置完成页面相关内容渲染。1. 每个组件均是由元数据驱动的标准化组件,分为组件和配置两部分;
2. 将后端返回的组件配置信息和本地默认配置merge后生成最终配置;
3. 组件配置引擎通过解析组件配置,动态化的绑定事件、传递数据、渲染组件;引入配置化引擎设计后,前端只有在新增组件类型时才需要投入开发人力,对于其他同质化需求仅需后端修改配置即可生效,极大降低了开发和测试回归成本。
V3 - 升级体验
3.1 背景
线索列表是线索管家的核心业务场景,随着业务的不断发展,急需对线索列表的体验进行升级。3.2 设计目标
秉持信息易懂、体验易用的设计价值观,通过对列表信息进行重组和体验升级,从而提升线索列表客户满意度。3.3 设计思路
3.3.1 页面拆解
线索列表页由标题、工具栏、表格三大模块构成;- 标题区:包括标题、规则说明等信息,概括整个页面的信息;
- 工具栏区:包括数据过滤(筛选、搜索)、功能操作(新建、导入...),承载页面的增、删、改、查等操作;
- 表格区:包括表头、表体、分页,展现页面的核心信息。
3.3.2 核心痛点
- 标题区:规则不明确,理解成本高;
- 工具栏区:数据过滤及功能操作密度大,干扰信息繁杂;
- 表格区:核心信息展示屏效比低,操作成本高。
3.3.3 设计策略
1. 标题区升级
- 【标题区】规则说明由图标优化为图标+文字形式,直观易懂;
- 【功能操作】外露重要高频操作,折叠低频操作,降低对用户的干扰;
- 【数据过滤】外露高频筛选,折叠低频筛选,提升核心表格区屏效占比;
- 【表头】页面上滑,表头吸顶展示,易于定位字段信息,拓展线索列表纵向空间;
- 【表体】页面横滑,横向滚动条悬浮于屏幕底部,提升横向操作效率;
- 【表体】操作列采用主次形式,外露高频功能,折叠低频功能,有效提升操作效率和功能扩展空间。
3.4 设计收益
通过构成、交互、反馈、适配四个维度将页面信息进行深挖与重组,核心表格区屏占比达到70%,数据展示更合理,提升线索列表页面易用性与客户满意度。V4 - 升级检索能力
4.1 现状和挑战
目前系统线索预置字段58个,动态扩展字段125个以及亿级线索数据。业务不断升级所有线索字段纳入自定义检索(多条件检索,分词检索等)和日益膨胀的数据让检索的性能面临挑战, 我们使用ES替代MySQL来实现高性能的高级检索功能。4.2 方案设计
名词解释-WATT:百度数据流开放平台-瓦特(WATT), 是数据流自助开发和运维平台。数据流捕获mysql数据库中变化数据实时发布出来, 以增量和基准两种方式进行发布, 提供文本格式数据,保证实时性、数据的有序和不丢,还可以对订阅数据进行计算。
方案:检索由DB迁移到百度云ES服务,用户操作完线索后数据更新到DB, 再通过WATT、MQ和写入服务将数据同步到ES。收益:提高查询效率, 支持全文分词场景,数据进行相关度排序等,支持任意检索条件组合查询不影响查询性能。
V5 - 支持写后读(Read your writes)
5.1 现状和挑战
线索列表检索由MySql迁移到ES后,又带来了新的问题,DB在同步ES的过程中,容易受依赖环境影响导致数据更新有延迟,导致用户在线索列表看到的不是最新数据,对用户体验有一定的影响, 从而对线索列表的稳定性和准确性带来了新的挑战。
数据流现状如下:
上述中DB同步ES共需要5步,任何一步出现延迟都会造成线索列表数据更新不及时。
5.2 优化目标
即使DB到ES数据同步存在延时,也能保证线索列表中的数据与用户操作后的结果一致。5.3 方案设计
1、用户对线索进行操作(新建,分配, 编辑等)后, 将操作后的线索ID写入到redis有序队列中,由于DB同步到ES延迟时间大概在0~2s间,因此redis队列自动过期策略设置为10s。
2、redis队列只写入线索ID,而不是存储完整的线索数据快照: a、线索属性多、结构复、迭代变化快,只记录线索ID比较轻量级,线索本身变更不影响redis中缓存的数据结构;
b、查询时根据ID从DB中查询,数据更准确,因为没有事务去保证redis与DB中的数据强一致性。
3、用户查询线索列表数据a、获取redis有序队列中最近5s的线索IDb、组装ES筛选条件查询ES数据,获取redis线索ID筛选DB数据, 为了保证列表的查询性能,采用并发查询c、通过表达式引擎,根据ES筛选条件过滤DB线索数据,获取有效的查询结果d、通过merge策略完成ES数据和DB数据merge
merge 策略为了能够简单阐述merge的各个场景,我们先定义一下几个对象1、leads_es:线索对应的ES结果视图
2、leads_db:线索对应的数据库视图
3、result_es = {....} :ES查询结果的结果集
4、result_redis_db = {....}:通过redis获取线索ID查询DB的结果集
5、result_redis_db_filter = {...}:result_redis_db根据筛选条件通过表达式引擎过滤后的结果集
6、candidate = {....}:最终融合到的结果集我们称之为候选结果集
candidate 选择策略1、有新增或修改操作后的线索且满足过滤条件的,应该把DB中查询到的线索加入候选集2、没有操作过的线索且满足过滤条件的,应该把ES中查询到的线索加入候选集3、操作过的线索且不满足过滤条件的,不应该加入候选集
效果收益:上线后无用户在反馈操作线索后列表数据延迟类问题,线索打标签后首次查得率、编辑线索后首次查得率均为100%。
V6 - 性能优化
6.1 如何度量性能
If you can't measure it, you can't improve it!页面性能度量是优化的前提,而前端性能监控也是一个经久不衰的话题。监控方向、监控指标、埋点方案、上报工具都是需要考虑的点,业界商用打点平台及厂内埋点平台,均无法解决易用、实时、前后端全链路监控等问题。为此爱番番启动「大前端APM」专项,参考服务端RED指标及业界主流前端埋点方案,探索最适合爱番番的前端APM体系架构。
6.1.1 监控目标
从全局问题出发,能够洞察统计性的页面url真实性能指标,以及可以做操作流任意阶段之间的统计性耗时分析。从个案问题出发,能够基于用户id进行任意一次全端调用链追踪。6.1.2 解决方案
采集
埋点SDK:爱番番业务打点使用付费系统「神策」,为避免重复造轮子,前端APM SDK基于神策SDK进行二次封装,通过npm包形式进行版本管理;使用方在公共模块对埋点SDK进行初始化。增加无侵入性能采集能力,提供采样率等配置扩展能力。上报
综合image、sendBeacon、Ajax上报方案。首先拼接携带参数的完整的url,判断url的长度,如果url的长度小于浏览器允许的最大长度内,则通过动态创建img标签的形式来发送前端性能数据;若url太长,则判断浏览器是否支持sendBeacon方法,如支持则通过sendBeacon方法来发送请求,否则发送同步的ajax请求。示例代码如下:
function dealWithUrl(url,appId){ let times = performanceInfo(appId); let items = decoupling(times); let urlLength = (url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&')).length; if(urlLength < 2083){ imgReport(url,times); } else if(navigator.sendBeacon){ sendBeacon(url,times); }else{ajaxReport(url,times); } };
前后端Trace打通
6.2 性能问题&目标问题:前端APM在2021年Q2全面落地,使得爱番番前端性能监控迈出一大步。但同时暴露出不少页面性能较差、大数据量用户可感知时长较长等问题。其中线索列表性能问题尤为突出,页面初始化用户可感知时长超过5000ms(P90);目标:通过对爱番番所有页面性能统计分析,前端整体性能指标为「页面初始化用户可感知时长低于2000ms(P90)」,分段拆解目标如下:
6.3 优化思路&方案整体优化思路及节奏:先抓主要矛盾,摘低垂果实;再抠细节,进行难点攻坚。每个方向均做到极致,各个击破。
- 【链路】厘清页面完整链路及耗时,全面了解页面性能现状。
- 【后端】深入分析后端代码实现逻辑,从并发、缓存、es调优、线程池调优,依赖超时分析等方面着手优化接口性能。
- 【前端】分析前端代码、编译配置、浏览器Performance火焰图,从静态资源体积、JS Runtime Long Task、渲染性能等方面入手优化前端性能。
- 【交互】与产品、设计共同探讨交互升级方案,以达到极致用户体验。
6.3.1 耗时链路分析名词解释:BFE,Baidu Front End,百度统一前端,是百度统一的七层(HTTP/HTTPS)流量接入平台;为整个百度提供流量接入服务。
用户可感知耗时链路:
- 从CDN节点加载静态资源(静态资源耗时)
- 执行js文件,发送Ajax请求(接口总耗时开始)
- 浏览器发出Http请求,抵达BFE的网络耗时
- BFE转发至Access Gateway链路耗时
- Access Gateway转发至BFF服务链路耗时
- BFF自身服务耗时(包含服务内部请求、拼装微服务等),由Skywalking统计
- BFF返回response,至Access Gateway链路耗时
- Access Gateway返回response,至BFE链路耗时
- BFE返回response到浏览器网络耗时
- 浏览器渲染耗时
问题:
- BFF Node.js 服务调用后端微服务链路耗时长。
- BFF通过域名-BFE-微服务方式调用时,BFE部分VIP存在连接超时问题无法彻底解决。
优化前BFF调用链:
方案:
-
BFF模块升级Mesh服务。BFF调用后端服务方式,由网关域名调用,升级为Mesh service调用。
收益:BFF调用微服务链路耗时降低100ms+,BFE VIP连接超时频发问题得到根治。
6.3.2 接口性能优化通过分析接口实现逻辑以及SkyWalking调用链,发现下面三类问题:第一类问题:代码实现问题。属于低垂的果实比较容易摘到。问题:
- BFF及后端接口链路存在非必要串行请求
- 前后端均存在可以使用缓存场景
- 在BFF实现层面,将三次串行请求,优化成必要的两次串行,其余均使用Promise.all并行处理
- 对服务端接口后置处理器,统一使用线程池异步处理
- 对标签元数据、表头配置元数据等接口增加Redis缓存
- 前端对权限、查询条件等接口进行预加载,并增加本地缓存
第二类问题:性能调优。这部分需要了解ES、RPC框架底层实现原理。问题:
- 使用ES检索模糊查询性能较差
- 线索服务压测不达标,高QPS下请求排队
- 使用全文检索来代替模糊查询
- 调整RPC调用RPCIoWorkThreadNumber工作线程数
第三类问题:超时问题。这部分优化场景复杂,难度较高。超时问题有几类:问题:
- RPC调用连接超时:老版本Mesh sdk每次发起请求时会重新创建新的连接,导致调用方在并发大或者访问的服务平响高时,会出现连接等待,以至连接超时。
- 网关查询超时:线索所属DB集群 sfcrmsales磁盘利用率较高,慢SQL及长事务较频繁,DB集群稳定性不高,导致DB查询频发超时。
- 依赖外部服务超时:线索列表需要对线索池权限等进行鉴权操作,依赖ACS团队权限接口超时频繁;列表检索完毕后,需要对部分字段进行加工,涉及不少外部团队HTTP请求调用,其中大商业广告信息open API调用超时最为频繁。
- 设立DB稳定性治理专项,增加DB告警、值班机制;
- 联合DBA对慢SQL、长事务、连接数异常、主从延迟高等问题进行彻底分析、排查、解决、验证、根治;
- 对DB中重试表、分配日志表、outbox表等大表进行清理,对线索原始信息主表进行定期数仓转储,并清理表中大字段;
- 对集群中非一级业务表进行迁库操作,减轻线索DB集群存储压力。
- 调整ACS服务线程池大小,对ACS服务进行水平扩容;
- 对广告信息接口进行 缓存+离线数仓+实时调用 组合查询方案,并调整后置处理器超时时间,保证绝大部分场景查询速度。
收益:线索列表服务端耗时(P90)从2500ms+,缩短至800ms以下。
6.3.3 前端性能优化
前端性能问题主要集中在渲染卡顿和静态资源加载耗时长两方面。第一方面:列表渲染优化背景:爱番番前端整体采用vue技术栈,elementUI作为vue生态中最受欢迎的UI框架,因其强大的功能、健全的生态以及活跃的社区,被选做爱番番前端UI框架。线索列表使用element-ui中el-table组件,能够支持列表场景常见需求。
问题:1.大数据量(100行、50列)情况下,JS Long Task时长将近3000ms,线索列表渲染卡顿。2.数据整体渲染时间较长,用户等待(loading)时间长。3.el-table不支持吸顶、吸底操作,单纯通过修改CSS样式亦无法实现,体验不佳。
分析:阅读el-table源码发现,el-table的实现是通过四个HTML 原生table(表头、表身,列表左右侧勾选列)实现。大数据量下,DOM数量会变得异常庞大;尤其是在表头拖拽,对页面进行Repaint和Reflow时计算量巨大,Long Task较长,导致页面卡顿。调研基于动态渲染思路的开源组件pl-table,不支持吸顶功能,且在行高不固定的情况下支持情况不良,被排除。
方案:1.通过调研业界主流UI框架,只有React体系中的AntD table支持吸顶功能,但其vue版本存在滞后性并未支持该提醒。经过组内讨论,决定参考React AntD table实现思路,重新实现AFF-UI table。2.列表数据渐进式渲染。先渲染首屏数据,结束loading为用户呈现首屏列表。同时在用户无感知的情况渲染其他列表数据。
收益:1.AFF UI table相比el-table,相同数据量下,DOM数量减少60%,100行、60列数据量下,渲染性能提升300%;2.支持列表吸顶吸底功能,体验提升较大;3.渐进式渲染,首屏可感知渲染时长(P90)从2000ms降低至500ms以下。
第二方面:静态资源优化背景:爱番番前端使用自研Tangram微前端架构,分为主模块「common」,以及其他业务域子模块。支持各敏捷小组页面分代码库,独立部署运维。前端静态资源均部署在百度云BOS上,通过BFE进行域名静态资源转发定位。
问题:
- 静态资源经过BFE转发,增加链路耗时
- 静态资源未使用CDN网络
- 静态资源体积较大
分析:静态资源优化思路,从链路、缓存、编译三方面入手。链路和缓存,是前端优化常见手段,方案大同小异。编译的问题一般隐藏较深,需要对webpack编译打包原理有一定了解。下面着重介绍下编译相关分析过程:1、首先通过监控平台,对js加载瀑布流进行分析,定位到chunk-vender.js存在耗时较高的情况,为性能瓶颈。
2、通过 webpack analyzer 对编译情况进行分析:
发现存在以下问题:
- 业务域代码中chunk-vendors.js打包了微前端框架底座 biz-crm-fe-common模块,这部分能力可以在主应用中提供,业务模块打包是应该排掉。
- 部分依赖库(bce-sdk、tangram-ui、moment、lodash)体积巨大,需要考虑作为公共依赖放入主应用,或者在页面通过Promise异步加载。
- 代码库路由较多,复用组件较多,导致chunk-common.js体积较大,且静态资源整体体积大。
方案:链路:
- 静态资源直接请求BOS域名,不走aifanfan.baidu域名,减少一层BFE转发链路
- 开启百度云BOS CDN加速
- 开启静态资源gzip压缩
- 在首页中预加载线索列表相关静态资源
- 在HTML预加载线索列表相关JS,利用webpack require特性,全局缓存列表相关JS(该方案是方案1的进阶版,预加载更前置、彻底)
- 修改微前端框架tangram-sdk编译配置,排除biz-crm-fe-commom包
- 拆分前端代码库。将列表和详情之外的非核心页面迁移新代码库,保证列表js体积最小化
- 进行异步加载大体积依赖js,使得初始化加载js体积压缩到极致
- 静态资源耗时(P90)从 2000+ms,优化至500ms以下
- 静态资源gizp总体积:693.52KB → 228.83KB,总体积缩小67%
附编译优化前后js体积对比(飘绿为优化后)
6.3.4 体验优化
问题:- 列表全屏loading,白屏时间长;
- 列表筛选条件全部、实时加载,客户感知时间长,且会有抖动;
- 表头数据默认值可全选(50+列),请求、渲染数据量大。用户在使用时需要横向滑动3-5屏,体验不佳。
- 列表筛选使用侧拉面板交互,本地记忆上次筛选条件并展示;
- 优化loading区域及方式,从视觉感知上最小化用户感知时长;
- 通过对所有用户自定义表头设置数据分析,及参考业界通用设置,增加表头数量上限。
6.4 收益
经过上述优化手段,线索列表整体性能达标。
回顾将近一年的性能优化旅程,大致经历了下面几个阶段:
- 「无从下手」用户反馈列表性能存在问题,尝试解决,但无法度量,在黑暗中摸索。(20年Q4 — 21年Q1)
- 「磨刀不误砍柴工」调研前端性能监控解决方案,启动爱番番大前端APM Topic。(21年Q1)
- 「有的放矢」APM建设,web端全面落地。对线索列表性能有全面了解,优化工作逐步清晰明朗。(21年Q2)
- 「先摘低垂果实」使用常规手段,对前后端常见性能问题进行解决和优化。(21年Q3)
- 「难点攻坚、毫秒必争」不放过任何一个优化点,对前后端难点问题分别成立专项小组,集思广益,重点攻坚,不拿结果誓不罢休。(21年Q4)
三、总结
「以客户为中心,技术为产品服务」是爱番番线索管家团队一直遵循的原则。技术架构的规划首先应该围绕业务诉求展开,用合理的技术赋能产品,产品在不断的演进中对技术提出更高的标准和要求。线索列表是爱番番众多核心功能其中的一点,但管中窥豹,技术问题往往要从业务中寻找答案,理解业务是开展技术的前提。同时,业务的复杂度升级反哺技术架构进化。业务与技术相互促进,相辅相成,最终为用户创造价值。产品和技术演进只有起点,没有终点。展望未来,线索列表能力通用化,配置扩展能力向PaaS平台演进,LowCode及NoCode等方向均需要继续探索实践,我们要走的路还很长。
四、作者介绍
本篇系爱番番线索管家团队多位同学共同编写。- 飞邪:架构师,擅长通过微服务架构和DDD落地复杂系统
- TJ:设计师出身,努力向服务端转型的前端开发
- Hana:一只耕耘在To B行业的设计狮
- 东拾:资深研发工程师,擅长业务系统架构设计
- 三木: web前端工程师,擅长各种撸猫