本文旨在梳理目下通用的性能优化方法

性能优化没有一个统一的指标,但目的是明确的,就是要让站点应用的加载速度够快,如此用户体验才能高(暂且不管页面做的烂不烂),毕竟从应用开发者的角度看,时间就是金钱。为了让时间压缩到极致,我们需要从请求到完全响应的全部细节入手,尽可能的压缩。即:从用户在地址栏中输入—>页面加载完成,该过程中的每一个点,都需要倍加注意。可能这就是为什么那么多面试官那么喜欢问一道烂大街的题: 地址栏里输入地址后到直到页面展示,尽可能详细地说说…

精髓部分

根据《高性能浏览器网络》的研究结论得出,性能优化三个大的角度:带宽、延时和渲染耗时.那么由于现代光纤通信的大规模普及,带宽的影响因素,已经小到可以忽略不计的地步(管道宽到你怎么都塞不满),所以本文不讨论,剩下的就是延时和渲染

  • 延迟耗时层面
    《计算机网络》的第六版中,对于延迟给出了明确的定义,从c端发出消息到s端收到消息的时间消耗,具体包括:排队延迟 –(缓冲)-> 处理延迟–(路由)->传输延迟—>传播延迟。但是扯了这么多是基于一个大前提的,就是真实发送请求的情况下。引用《高性能浏览器网络》的终极解决思路:

    没有请求,就是最快的。

有点废话的意思,但至少也是个思路,能不发则不发,能少发就少发。依据这一原则,我们可以有如下的措施:

  1. 能不发就不发。既然要不发送请求,那就是直接从本地读取。所以要使用缓存策略,具体实践:开启cache-control,缓存资源。
  2. 能少则少。所以就极尽能事的合并请求,具体实践:精灵图整合众多小图资源等。
  3. 能近则近。我们不能让数据传输得更快,但可以让它们传输的距离更短-CDN。同样大小的资源,距离用户越近,耗时理论上越小(网速差距不大)
  4. 能小则小。首部压缩,后端gzip压缩,减少资源重量,加快传输速度。
  5. 对于一些小的资源比如小图,可以通过base64编码将其变成字符串直接嵌入到页面中,也不用发送请求。
  6. 甚至说,对于一些需要返回百万级别数据量的接口(项目中就碰到过如此奇葩的需求),我们可以通过传文件压缩包的形式给到前端,然后前端自行解压缩获取其中的数据。重点是要尽可能地把数据压扁。
  7. 坚决屏蔽重定向。
  • 渲染层面
    1. script标签defer或者async。由于在解析html时若碰到js代码就会牵扯到进程切换带来的开销且会阻塞html的解析,因此通过这俩属性规避之。图片描述

2. 减少重排重绘带来的不必要的开销。

  • 防抖、节流该加的加上,屏蔽用户无脑操作。
  • 图形化编程时能少画的就少画,能不画就不画。
  • 直接用js改样式的炸裂操作,少做

3. 虚拟列表
实现思路: 设置监听器监听用户是否滚动到底部,或者顶部。如果触发,当前数据的最后一项以参数传给后端,后端根据参数,返回对应的后面数据, 前端拿到拼接渲染。当然,也可以纯前端实现,有现成的库可供调用,如react-virtualized、react-window等。

4. 图片压缩、预加载、懒加载

  • 压缩自不必说,能压则压,目下前端层面推荐格式webp。

  • 预加载:说白了就是提前加载。我们对目标图片预先请求一次,这样后面再次请求时,就会走本地缓存。

  • 图片懒加载:这个东西如何实现的呢?
    思路: 当图片item出现在可视范围内,再去加载图片。
    实现:

    1. img标签的loading属性,设置为lazy。但是这种方式存在小瑕疵,当图片在靠近可视范围但还没有出现在可视范围内时, 图片会预先加载,就是不精确。
    2. 监听scroll事件
    3. intersectionObserver
    // 监听scroll事件
    const iniObserver = () => {
      if (!myRef.current) {
        return;
      }
      console.log(myRef.current); // 这是原生的DOM对象
      let options = {
        threshold: 1.0,
      };
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry: any) => {
          if (entry.isIntersecting) {
            entry.target.style.opacity = 1;
          } else {
            entry.target.style.opacity = 0;
          }
        });
      }, options);
    
      observer.observe(myRef.current);
    };

    相当于设置一个监听,dom出现在视图内isIntersecting即为true。此时再去执行后续的逻辑

  • 其他方面

  1. DNS预解析甚至tcp链路预链接
    假设页面中有几个超链接。用户没有点击的时候我就已经预解析了域名ip甚至链路都连接好了,这样,当用户点的时候,只剩下发送资源请求的耗时。
  2. 长连接。减少建立链路的耗时。这是浏览器层面的优化(无需关注底层已实现)
    截止到http1.0版本的协议。请求响应都是遵循请求、响应、断开。周而复始。1.1版本最大的优化就是实现长链接,复用链路。

3. 分析包的大小

// 安装
cnpm install source-map-explorer
// 配置命令
    "analyze": "source-map-explorer 'build/static/js/*.js'"
  1. React专属优化手段
  • 类组件的shouldComponentUpdate
shouldComponentUpdate(nextprops, nextstate) {
  if (nextprops.text === this.props.text) {
    return false
  } else {
    return true
  }
}

通过在shouldComponentUpdate生命周期中的判断逻辑,确定是否重新渲染, 由code可知,分为了false(不渲染)true(重新渲染)两种

  • 类组件中的pureComponent
    相当于帮我们自动进行了比较的操作,但是注意,这里的比较,是浅比较。此时,需要结合Immutable.js,完全捕获变化,以确定是否更新。

  • 函数组件的React.memo
    这个类似类组件的pureComponent
    这个机制的作用在于,让子组件只有在props变化了的情况下,才会重新渲染。否则,每次父组件状态变化重新渲染,子组件会跟着操作。

// 父组件memo包裹
import { useEffect, useMemo, useState, memo } from 'react';
import ChildComponent from './ChildrenComponent';

const MemoSon = memo(ChildComponent)
...
...
<MemoSon name={name}/>

// 子组件
const ChildComponent = (props) => {
  const { name } = props
  console.log('儿子组件渲染')
  return (
    <div>
      <div>我是{name}...</div>
    </div>
  )
}
export default ChildComponent

效果如下:

  • 函数组件的useMemo
    更加精细化的复用组件的某一个或某几个部分
    const Child = (props) => {
        const { count } = props
        console.log('儿子A渲染1')
        const content = useMemo(() => {
        console.log('儿子A渲染2')
          return count
        }, [count])
        return (
            <div>
              { content.name }
            </div>
          )
      }