首屏优化

71 阅读4分钟

前言

  1. 首屏优化是指用户第一次进入应用的页面,不一定是首页
  2. 首屏的优化对公司来说非常重要,如果是c端产品会导致客户的流失,如果是B端的产品会直接影响成交,现有用户的续费,将来新用户的成交

背景

公司的产品面向企业用户,大部分企业用户会对产品进行私有化部署;企业对产品的需求是差异化的,因此公司研发了一套低代码平台方便客户对产品进行差异化定制;客户通过低代码平台的定制会产生js文件,这些js文件会在运行时加载。

大量客户反馈首屏等待时长过长问题,需要制定针对性的优化策略解决问题;而要针对性的优化必须要弄清楚目前的性能热点在哪里,因此,我们将优化策略分三步走:

  1. 查看问题,收集信息和分析

    • 定位问题;猜想,根据猜想收集信息解决问题,反馈应证猜想
  2. 解决问题,制定实施措施

  3. 反馈效果,查看解决效果

    三步可以反复轮询直到问题解决

要落实这三步需要在技术面提供如下支持:

  1. 服务监控系统的更改(前后端)
  2. 数据脱敏(大数据)
  3. 数据展示(前端)
  4. 问题实验室(前端、Bff)
    • 做AB实验定位问题

最终问题定位如下四点

  1. http1.1协议效率低下问题
    • 队头阻塞
    • 头部臃肿
  2. 大量的js代码重复
  3. 大量js代码无差别加载
  4. 大量的Api网络请求

问题解决

  1. http1.1效率低下问题

    • 队头阻塞问题
      • 在同一个tcp连接中,请求是响应必须按顺序处理,前面的请求未处理时,后面请求需要等待;
      • 解决方案
        • 多开域名来解决,浏览器同一个域名可以支持最多六个tcp并发连接,超过这个数后会发生队头阻塞
    • 头部压缩问题
      • http1.1不支持头部压缩,而发送的请求中包含大量的自定义头部
      • 解决方案
        • 借鉴http2的头部压缩方式,对自定义头部进行了压缩处理,客户端和服务端共同维护一个词典(动态\静态),发送时根据词典数字含义发送,服务端解码数字,每个数字代表不同的含义
  2. 针对重复代码的优化措施

    • 客户侧产生大量的差异化js文件,每个js文件的产生逻辑非常简单,结果就是客户侧保存了大量的js代码文件,并且代码文件中出现了大量重复代码;因为代码是在运行时产生的,无法使用树摇,因此对重复代码的提取必须动态完成; //考虑如下俩段代码,如何找到重复 //片段1 const selectSource = 'department'; const source = getData(selectSource) bindSelectSource({ source, label: (s)=>${s.name}, valeu: (s)=>${s.id} }) //片段2 const selectSource = 'task'; const source = getData(selectSource) bindSelectSource({ source, label: (s)=>${s.name}, valeu: (s)=>${s.id} })
    • 主要解决俩件事:
      • 如何找到重复代码
        • 使用ast,对比、比较代码片段是否相同 图片.png

        • 找出俩棵树连续的相同结构的节点,将他们提取成函数,同时把相同函数中的不同点提取为函数参数,大致思路是:

          1. 计算每个节点的结构hash
           //变量声明节点的hash求值
           class VariableDeclaration {
               // ...省略其他代码
               hash(){
                   md5.append(this.kind); //const var let
                   md5.append(this.name); //变量名
                   md5.append(this.init.type); //初始值类型:字面量、表达式、变量
               }
               return md5.end(); //得到hash
           }
          

          2.将ast树信息入库(含hash结果),库中的信息大致如下:

            [
              {
                 "filename": '1.js', //文件名
                 "struck":[
                     {
                         hash: "......", children: [
                           { hash: "..." , children: [] }
                         ]
                     }
                 ]
              }
            ]
          

          3. 寻找库中连续出现的hash一致的节点进行提重 4. 将差异点提取成参数 5. 最终提重的结果如下: function rp_2d4ef(p1,p2){ const selectSource = p1; const source = getData(selectSource) bindSelectSource({ source, label: (s)=>${s[p2]}, valeu: (s)=>${s.id} }) } 然后代码片段被修改为 //片段1 rp_2d4ef('department','name') //片段2 rp_2d4ef('task','title')

      • 什么时候提重
        • 为了保证效率,提重可以异步延迟执行,也可以开启计划任务在服务器空闲时执行,也可以管理员手动执行
  3. 针对无差别加载的优化措施

    • 用户侧自定义产生的js都是在首屏全部加载的,实际只需要加载首屏用到的js文件
    • 因此对视口内组件进行判断,视口内先加载,不需要的延迟加载
    • 需要做俩件事情
      • 定义每个功能页视口内组件
      • 标识每个js文件对应到哪个组件
  4. 针对大量API网络请求的优化措施

    • 有大量重复请求,可以加入暂时缓存,让相同的请求走缓存通道
    //如下请求代码
    const data = await getSelectSource('department')
    
    //修改为
    const data = await withCache(getSelectSource,'department')
    
    let map = new Map();
    function withCache(fn,...args){
        if(!map){
            return fn(...args)
        }
        
        let caches = map.get(fn);
        if(!caches){
            //无缓存,初始化
            map.set(fn,cache=[])
        }
        let cache = caches.find(c=>isSameArgs(args, c.args)) //按照参数查找缓存
        if(!cache){
            //无缓存 初始化
            caches.push(cache={
                args,
                value: fn(...args)
            })
        }
        return cache;
    }
    
    const CACHE_DURATION = 5000;
    setTimeout(()=>{
        map = null
    },CACHE_DURATION)