SunXinFei / sunxinfei.github.io

前后端技术相关笔记,已迁移到 Issues 中
https://github.com/SunXinFei/sunxinfei.github.io/issues
32 stars 3 forks source link

前端性能优化二三事 #25

Open SunXinFei opened 4 years ago

SunXinFei commented 4 years ago

前端性能优化是特别庞大内容区域,涉及浏览器渲染机制、框架源码、网络请求...这里做一些总结。比较好的总结:google developers

SunXinFei commented 4 years ago

长列表优化

长列表业务上比如1w+的table列表、feed流(twitter、ins),这一类的长列表一般都是伴随着dom元素与绑定数据越来越多,加载到一定的程度会变得非常卡顿。优化的痛点就是减少可视范围内的dom元素数量。

占位符

从结构上分为单行元素item定高与不定高的情况。定高的情况相对来说就很简单,我们只需要判断当前元素是否在可视范围,如果超出可视范围,便用相同的高度占位元素进行替换。关于如何判断一个dom元素是否在可视范围,在懒加载这个模块进行详细讲述。不定高的元素,则需要计算/手动传入该item元素的高度。

padding与transform

除了刚才提到的占位符替换,还有padding和transform处理,以ins和twitter为代表的处理方法是分两种: 前提: 整个列表只容载20个item,至于每一个item是在滚动过程中数据刷新还是item结点进行摘取和挂载dom树我们这里先不讨论,比如我们滑出可视范围10个item之后,将这10个item的高度进行累加。

padding的情况处理: 我们在group上面修改css样式,padding的top值即为这10个item的高度和;

image

transform的情况处理: 每一个item的样式是

    position: absolute;
    top: 0;
    left: 0;

然后每一个item的纵向偏移之前元素的高度和,比如第一个元素:transform: translateY(0px);

image

(()=>{
   const onScroll = (e)=>{
      this.firstViewIndex = e.detail.applyIndex;//scroll触发修改firstViewIndex;
      //如果feed总长度减去当前的index值小于等于模版的长度,则请求新数据
      //eg. 20 - 14 <= 6 那么请求新的数据
      if (feed_list.length - firstViewIndex <= tmpNoop.length) {
          DeferFn(ajaxAction, 500)();
      }
    }
    const tmpNoop = new Array(6).fill(0);//6个占位符
    const firstViewIndex = 0;//当前看到的游标

    const calcIndex = (index) => {//通过firstViewIndex,计算tmpNoop中实际应该展示的index数组
      let indexShouldBe = index - (firstViewIndex % tmpNoop.length);
      if (indexShouldBe < 0) {
        indexShouldBe += tmpNoop.length;
      }
      let currentIndex = indexShouldBe + firstViewIndex;

      return currentIndex;
    }

    let ret = tmpNoop.map((_, viewIndex) => {
        let dataIndex = calcIndex(viewIndex);
        return dataIndex;
      });
    console.log(ret);
    //firstViewIndex = 0 => [0, 1, 2, 3, 4, 5]
    //firstViewIndex = 1 => [6, 1, 2, 3, 4, 5]
    //firstViewIndex = 2 => [6, 7, 2, 3, 4, 5]
})()

twitter、ins分别是上面的两个思路的实现体, 另外两个开源项目可以进行参考: https://github.com/tangbc/vue-virtual-scroll-list https://github.com/Akryum/vue-virtual-scroller

轮播图

综上所述这种思路:dom元素并不一口气for循环挂载到dom树上,而是通过事件来控制挂载和摘取或者复用dom。这种思路也适用于轮播图切换,比如一共有10张轮播,我们每次控制3个轮播图占位符的挂载,第一次挂载index为0,1,2这三张,随着滑动,我们挂载index为3,1,2,再滑动index为3,4,2,代码层面通过滑动改变index,从而计算3个轮播占位符的数据即可。

// 轮播滑动事件处理
  onSlideChange (event) {
      const index = event.detail.index
      this.activeIndex = index
      if (index == 0) {
          this.slideIndexes.splice(0, 1, 0)
      } else {
        switch (index%3) {
          case 1:
            this.slideIndexes = [index - 1, index, index + 1]
            break;
          case 2:
            this.slideIndexes = [index + 1, index - 1, index]
            break;
          case 0:
            this.slideIndexes = [index, index + 1, index - 1]
            break;
        }
      }  
    }
    first () {//第一个轮播占位符
      return this.items[this.slideIndexes[0]] || {}
    },
    second () {//第二个轮播占位符
      return this.items[this.slideIndexes[1]] || {}
    },
    third () {//第三个轮播占位符
      return this.items[this.slideIndexes[2]] || {}
    }
SunXinFei commented 4 years ago

懒加载

懒加载泛指元素由不可视范围内=>可视范围内,元素由未加载到加载的过程。此常用来做图片处理,目的是为了减少http的请求 ,减少服务器端压力,尤其是图片较多的页面,效果尤为明显。主要实现思路是三种:

  1. 偏移量 应该是最原始的方法,利用的就是元素和可视区域的API-offset。 公式:el.offsetTop- scrollTop <= viewPortHeight.
    function isInView (el) {
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const offsetTop = el.offsetTop
    const scrollTop = document.documentElement.scrollTop
    const top = offsetTop - scrollTop
    return top <= viewPortHeight
    }
  2. getBoundingClientRect 利用的就是该API返回元素的大小及其相对于视口的位置,常用来做Vue/React项目里面的拖拽等操作逻辑内的一些判断:碰撞检测等。 公式:el.getBoundingClientReact().top <= viewPortHeight
    function isInView (el) {
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
    return top  <= viewPortHeight
    }
  3. Intersection_Observer_API 如描述,可以做如下功能:图片懒加载、内容无限滚动、检测广告的曝光情况、在用户看见某个区域时执行任务或播放动画 。 其实运用这个原生的交叉逻辑的回调函数,能做很多事情,还比如说碰撞检测,但是要注意的就是兼容性的问题,如果是toC的pc端,不建议使用。
    // 定义一个观察器
    const observer = new IntersectionObserver(ioes => {
    ioes.forEach(ioe => {
        const el = ioe.target
        const intersectionRatio = ioe.intersectionRatio
        if (intersectionRatio > 0 && intersectionRatio <= 1) {
            loadImg(el)
            io.unobserve(el)
        }
         el.onload = el.onerror = () => io.unobserve(el)
    })
    })
    // 执行交叉观察器
    function isInView (el) {
    observer.observe(el)
    }
SunXinFei commented 4 years ago

服务端渲染、预渲染、客户端渲染

image

SunXinFei commented 3 years ago

transform与合成线程(Compositor thread)

transform涉及到性能优化除了使用GPU加速之外,还涉及到合成线程; 合成的好处在于其独立于主线程。合成线程不需要等待样式计算和Javascript代码的运行。这也是为什么合成更适合优化交互性能,但如果布局或者绘制需要重新计算则主线程是必须要参与的。 GPU会加速的属性:transform、opacity、filter 附加: 浏览器进程:浏览器进程、渲染进程(GUI渲染线程、js引擎线程、io线程、定时器线程、事件触发线程)、GPU进程、网络进程、插件进程 每一个页面分配一个渲染进程,一个渲染进程由多个线程相互协同

参考: https://xie.infoq.cn/article/5d36d123bfd1c56688e125ad3?utm_source=tuicool&utm_medium=referral http://sy-tang.github.io/2014/05/14/CSS%20animations%20and%20transitions%20performance-%20looking%20inside%20the%20browser/ http://www.cainiaoxueyuan.com/zhizuo/8484.html https://segmentfault.com/a/1190000008650975 https://zhuanlan.zhihu.com/p/78230297

SunXinFei commented 3 years ago

繁星

http层面:

  1. 强缓存、协商缓存
  2. 请求合并减少请求数量
  3. http2.0 多路复用和头部压缩
  4. gzip

静态资源方面:

  1. 使用CDN存放静态资源
  2. 散的图标替换成字体文件
  3. 多个图合成雪碧图,减少网络请求数量
  4. 小的图片打成base64,减少网络请求数量
  5. 懒加载图片
  6. 评论区图片使用图床等工具,进行分辨率以及切割压缩小图
  7. 减少使用gif
  8. 字体优化

webpack层面:

  1. js与html以及css的压缩
  2. splitChunk进行合适体积大小的切割
  3. 将其他页面进行lazy-load

代码层面

  1. Vue中使用计算属性优于方法
  2. React中使用SCU来控制组件Render
  3. MVVM的框架,props的数据尽量进行精简
  4. Redux使用时,组件未用到的属性不要connect,防止无意义Render
  5. 对dom的操作,集中书写,防止出现dom操作与dom查询交叉
  6. 利用事件冒泡,将多个子组件的事件绑定到父组件身上
  7. transform比top与left性能高
  8. 防止reflow
  9. css选择器要减少层级以及复杂的选择器书写