得物技术 2024年12月16日
站外商详的重构与优化|得物技术
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文讲述了得物技术团队对站外商品详情页进行重构的实践。为了解决站外商详与App商详体验割裂、前端性能较差的问题,团队采用了源码搭建方案,并针对首屏性能、同构运行、风险控制等方面进行了优化。通过拆分首屏数据接口、重构请求拦截器和埋点Hook等技术手段,显著提升了页面加载速度和用户体验,同时保障了数据安全。重构后的新版商详在性能和业务指标上均取得了显著提升,为后续功能扩展和多环境集成奠定了基础。

🚀 **性能优化**:通过采用源码搭建和SSR技术,并拆分首屏数据接口,显著提升了页面首屏加载速度,平均FMP提升61.4%,LCP提升63.5%。

🛠️ **技术架构升级**:放弃了原有的uni-app多端同构方案,采用SSR架构的源码搭建,同时保证了代码层面的同构开发,并设计了新的运行流程以适配多端环境。

🔒 **安全与体验平衡**:在数据安全与用户体验之间找到平衡,通过拆分首屏数据和完整数据接口,在保证数据加密的同时,实现了首屏内容的快速渲染。

🧩 **模块化重构**:对请求拦截器和埋点Hook进行了重构,使其更加模块化、可测试和易于维护,同时提高了埋点上报的效率和准确性,减少了前端开发人员的心智负担。

📈 **业务指标提升**:重构后,H5和小程序平台的立即购买点击触达率和购买按钮点击触达率均有提升,表明用户体验改善带来了业务指标的积极变化。

原创 航飞 2024-12-16 18:32 上海

本文将简述为何我们决定重构站外商详:一方面可以接入得物后台最新版本的商详数据API,便于后续需求迭代,避免站外商详和App商详体验的持续割裂现象;另一方面可以同时提高站外商详的前端性能,带给用户更好的使用体验。

目录

一、背景

二、技术方案

    1. 源码搭建

    2. 首屏性能保障方案

    3. 同构与多环境运行

    4. 风险控制/止损策略

三、一些针对性重构

    1. 请求拦截器的重构

    2. 埋点Hook的重构

四、重构后的数据回收

五、总结

六、参考链接


背景

站外商详,功能较为单一

客户端商详,有丰富的类目差异化配置,功能众多


站外商详(H5/小程序)一直以来采用detailV3老接口数据,在样式和功能上,不能与最新版的客户端同步对齐,各个端之间的使用体验之间存在差异。


从唤端数据可以看出来,App商品详情页分享后的唤端成功率非常高,能够达到75%以上,代表着这些用户都是带有明确目标和意愿来App内部进入购买商品,ROI好的高净值用户:


而对于日均pv占据站外流量的TOP3的站外商详来说,唤端价值相对比较高的同时,uni-app多端同构方案的SPA架构限制了H5与小程序页面性能体验天花板,长期以来站外商详的性能指标在前端平台的性能统计大盘下比较靠后:



从上面的性能数据监控并对比源码搭建页面的性能数据可以看出来,旧版的商详性能数据并不理想,对用户在站外商详页的转化率有一定影响:


综上背景,我们决定重构站外商详,一方面可以接入得物后台最新版本的商详数据API,便于后续需求迭代,避免站外商详和App商详体验的持续割裂现象;另一方面可以同时提高站外商详的前端性能,带给用户更好的使用体验。


技术方案

我们本次站外商详升级到创新商详版本,放弃了原项目的uni-app多端同构方案,同时采用营销侧的技术基建——源码搭建;提高了站外秒开性能和用户体验,同时又保证了代码层面的同构开发,本文将详细介绍本次站外商详的重构与优化。


源码搭建

源码搭建是得物前端平台基于SSR架构的C端基建,本次商详重构采用源码搭建来完成重构任务,以下是源码搭建的简要介绍:


源码搭建介绍:

源码搭建是利用页面搭建器现有开发组件能力快速生产页面的开发方式,业务开发不需要关心公用组件、体验、性能和稳定性基础建设,只需要在建立好的页面仓库中开发业务代码即可,集成了流水线构建会自动帮助开发构建上传。


首屏性能保障方案


此时就有用户体验 & 数据安全的矛盾存在:


那么这之间的矛盾和冲突我们是如何解决的呢?


简要流程如下图所示:


同构与多环境运行

我们重构的主要目的是为了提高性能以及对接最新版服务端接口,但是又不能因为重构而放弃了以往uni-app架构下的多端同构优势,所以需要设计一套新的运行流程来适配SSR下的新商详。


H5环境下我们可以直接访问SSR架构下的新商详,但是小程序运行环境该如何运行呢?


风险控制/止损策略

对于pv较高且包含完整交易链路的站外商详来说,冒烟点和阻塞线上购买流程的故障是不可接受的,因此我们设计了相对来说比较完备的止损策略。


故障降级页面——旧版商详

新版商详上线后,旧版页面暂时不会下线,路径和代码依旧保持不变;因此可以作为降级页面,能够保障在新版商详出问题后无缝将流量切回旧版商详。


SSR故障降级

如果SSR侧的请求出现了不可用现象,只会影响简版数据接口的渲染,因此即便失败了也只是影响秒开性能,而不会中断正常业务流程。


灰度策略

结合前端配置中心,我们可以通过逐步灰度放量方式对命中灰度的用户采取跳转新版商详策略,同时灰度配置也可以作为紧急的回滚手段,在遇到故障时及时将灰度放量关闭,引导所有用户跳转旧版商详。


一些针对性重构

在商详页面的整体重构过程中,我们识别出了一些关键模块需要进行针对性的重构。这些模块的重构目标是确保它们能够有效地适配商详页面的整体架构变化,同时提升可拓展性。这些针对性重构帮助我们解决了现有迭代中的瓶颈,并在保证系统稳定性的同时,加速开发的迭代过程。


接下来我们详细介绍其中请求拦截器与业务埋点Hook的重构设计。


请求拦截器的重构

因为新版商详需要在多种场景(Node.js / 微信小程序 / 移动端浏览器)运行代码,同时可以预见的是后续会有更多场景(如:支付宝小程序等)加入运行环境。


为了保障后续更多运行环境拓展性和可维护性,我们重构了请求拦截器模块:


1、RequestInceptor类型定义:

通过从定义层面区分不同环境,可以有效保障拦截器运行在有效环境,也从逻辑底层避免了一些可以前置避免的类型错误(比如在node环境下访问window等):

export interface RequestInceptor<T = InternalAxiosRequestConfig<unknown>> {  (): {    // node环境的请求拦截    nodeEnv: (config: T, runtimeConfig?: RunTimeConfig) => Promise<T> | T;    // 浏览器环境的请求拦截    clientEnv: (config: T, runtimeConfig?: Pick<RunTimeConfig, 'query') => Promise<T> | T;  };}


2、RequestInceptor的具体实现:

每个RequestInceptor都是一个函数,根据环境返回不同的处理逻辑,示例代码:

const h5CommonHeaders: RequestInceptor = () => ({  // 不同环境下需要携带一些不同的request header  nodeEnv: config => {    config.headers['reqEnv'] = 'node';    return config;  },  clientEnv: async config => {    config.headers['appid_org'] = 'wxapp';    return config;  },});
const yunDunSDK: RequestInceptor = () => ({ nodeEnv: config => config, clientEnv: async config => { // 只需要在浏览器环境加载的sdk await yunDunLoad; return config; },});


3、inceptorsLoader和requestInceptorsCreator共同实现了请求拦截器的处理流程:

inceptorsLoader:

const inceptorsLoader = async (initialConfig: InitialConfig, inceptors: RequestInceptor[]) => {  const promiseList = map(inceptors, interceptor => {    return async (config: InitialConfig) => {      const { nodeEnv, clientEnv } = interceptor();        if (isInWindow) {          return clientEnv(config, config?.runTimeConfig);        } else {          return nodeEnv(config, config?.runTimeConfig);        }    };  });  const promiseListResult = await promiseList.reduce(    (promise, fn) =>      promise.then(config => {        return fn(config);      }),    Promise.resolve(initialConfig),  );  return promiseListResult;};



requestInceptorsCreator:

export const requestInceptorsCreator = (config: InitialConfig) =>  inceptorsLoader(config, [    // 通用的headers    h5CommonHeaders,    // 风控    yunDunSDK  ]);


这个函数是一个工厂函数,它接收一个config对象作为参数,用于创建并返回一个处理后的配置对象。


4、通过RequestInceptor的设计,结合工厂函数requestInceptorsCreator,可以灵活地添加、删除或修改请求拦截器,同时保证拦截器按照特定的顺序执行。这种方式使得请求处理逻辑更加模块化、可测试和易于维护。在实际应用中,只需要调用requestInceptorsCreator函数,传入初始配置,即可得到一个完整的、优化过的请求配置,然后可以将其传给HTTP客户端(如axios)来发起请求。


埋点Hook的重构

一直以来,埋点开发深受前端同学吐槽和困扰,因为大量的埋点逻辑都跟业务逻辑/视图渲染有着强绑定的关系,同时又不得不写大量的“模版式代码”,费心又费力。


本次重构基于React Hook重构了埋点上报的应用层逻辑,可以在组件内引入Hook进行自定义上报/曝光上报;能更加高效的基于不同平台的运行环境去上报指定的埋点参数。


埋点Hook实现层:


1、generateTrackConfig 函数核心代码:

const generateTrackConfig = (trackSend: Readonly<Array<{ name: string }>>) => {  return function createTrackConfig() {    const names = trackSend.map(item => item.name);
/** * 埋点名由三部分组成: * 例:trackEventName_1234_3210 * event: 'trackEventName' * current_page: '1234' * block_type: '3210' 可能不存在 */ // 这里将埋点平台的函数名拆分出具体的上报入参 const extractEventData = (current: string) => { const nameSplit = current.split('_'); const [page, block] = nameSplit.slice(-2); const isBlockTypePresent = /\d+/.test(page); const event = nameSplit.slice(0, isBlockTypePresent ? -2 : -1).join('_');
return { event, current_page: isBlockTypePresent ? page : block, block_type: isBlockTypePresent ? block : '', }; }; // ... // 这里统一通过reduce组装成埋点sdk所需要的trackConfig配置 return names.reduce((total, current) => { const eventData = extractEventData(current); total[current] = (platform: string, { transParams }: any) => createEventObject(eventData, platform, transParams); return total; }, {}); };};


2、useProTrack 函数核心代码:

export const useProTrack = <T extends string>(  { props, functionalRef }: { props: any; functionalRef: any },  trackSendProps: Readonly<TrackSend<T>>,) => {  // 这里注入核心的埋点sdk配置  useWithReactFunctionalTrack({    functionalRef: functionalRef,    functionalProps: functionalPropsRef.current,    useEffect,    createTrackConfig: generateTrackConfig(trackSendRef.current),  });   // 这里通过useEffect接入曝光埋点的监听  useEffect(() => {    ObserveTrackRef.current = new IntersectionObserver(handleIntersection);    return () => {      ObserveTrackRef.current?.disconnect();    };  }, []);    // 这里针对具体埋点函数做统一封装,保证TS类型完整  const trackFuncMemo = useMemo(() => {    return trackSendRef.current.reduce((result, item) => {      result[item.name] = (        trackParams?: Record<string, unknown>,        options?: { ele?: HTMLElement | null },      ) => {        // 这里针对曝光埋点和主动埋点做区分,统一收敛调用方式,通过入参区分        if (!options?.ele) {          track(item.name)(trackParams);          return;        }        const { ele } = options;        startToObserveMap.current[item.name]();      };      return result;    }, {} as { [key in T]: trackFunc });  }, []);
return trackFuncMemo;};


埋点Hook应用层:


1、获取埋点名,从埋点任务中复制,不要手动拼写:


 2、组件内引用useProTrack,并注册埋点名:

  const proTrack = useProTrack({ props, functionalRef }, [{ name: 'trackEvent_1234' },    { name: 'trackEvent_2345' },  ]);


3、主动上报类型例子:

    proTrack.trackEvent_1234({      button_title: '我知道了',    });


这里会有埋点名的类型提示,注意选择正确的埋点名:


4、曝光上报例子:

 <div  className={bindClass(styles.button)}  onClick={handleGoBuy}  ref={ele =>    trackFn?.trackEvent_1234_3210(      {        trade_type_list: tradeTypeListStr,      },      { ele },    )  }></div>


在HTML元素(不要写在react组件只能是HTML元素)中的ref钩子注册,语法相比主动曝光多了一个配置(见上图)


埋点Hook的重构收益


重构后的数据回收


可以看出来重构后的性能收益非常明显,对用户体验提升有比较大的帮助。


业务指标收益:


总结

本次针对站外商详页面的重构取得了显著的性能提升,给用户带来了更好的体验。


站外商详,作为得物站外链路的主要商品信息承载页,在本次重构之后也接入了最新的商详数据API,为后续功能扩展和后续的多环境集成也打下了良好的技术基础。


参考链接

https://web.dev/articles/lcp?hl=zh-cn


https://web.dev/articles/cls?hl=zh-cn


https://web.dev/articles/optimize-lcp?hl=zh-cn


往期回顾


1.如何以MLOps保障时效表达稳定性|得物技术

2.二十万分之一几率:if语句变do-while卡死问题分析|得物技术

3.得物新一代可观测性架构:海量数据下的存算分离设计与实践

4.盘点这些年搭建器在用户体验优化的实践|得物技术

5.Java性能测试利器:JMH入门与实践|得物技术


文 / 航飞


关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

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

扫码添加小助手微信

如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:


跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

站外商详 性能优化 SSR 重构 前端技术
相关文章