钉钉红包性能优化之路

06-01 1626阅读

一、业务背景

请客红包、小礼物作为饿了么自研的业务产品,在钉钉的一方化入口中常驻,作为高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-mobile
      extraBabelPlugins: [
        [
          '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的模块拆分成单独的chunk
                  test(module) {
                    return (
                      module.size() > 160 * 1024 &&
                      /node_modules[/\]/.test(module.nameForCondition() || '')
                    );
                  },
                  // 把剩余的包打成一个chunk
                  name(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之间增加钉钉标准loading
          api.modifyHTML(($) => {
            $('head').prepend(`
              
                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); }
                }
              
            `);
            $('body').prepend(`
              
                
              
            `);
          });
        };
        // 在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

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码