🔁 一、加载篇

“网站页面的快速加载,能够建立用户对网站的信任,增加回访率,大部分的用户其实都期待页面能够在 2 秒内加载完成,而当超过 3 秒以后,就会有接近 40%的用户离开你的网站。”

1、首屏加载

1.1 白屏及性能优化

  • 时间段:FP(First Paint) - performance.timing.navigationStart
    • 回车按下,浏览器解析网址,进行 DNS 查询,查询返回 IP,通过 IP 发出 HTTP(S) 请求
    • 服务器返回 HTML,浏览器开始解析 HTML,此时触发请求 js 和 css 资源
    • js 被加载,开始执行 js,调用各种函数创建 DOM 并渲染到根节点,直到第一个可见元素产生
  • 优化
    • loading 提示: webpack 插件html-webpack-plugin,配置 html 时可在文件中插入 loading 图
    • 伪服务端渲染:prerender-spa-plugin插件在本地模拟浏览器环境,预先执行打包文件,通过解析获取首屏的 HTML,在正常环境中,返回预先解析好的 HTML
    • 使用 HTTP / 2.0:
      • http2 采用二进制分帧的方式进行通信,而 http1.x 是用文本,http2 的效率更高
      • http2 引入多路复用,即跟同一个域名通信,仅需要一个 TCP 建立请求通道,请求与响应可以同时基于此通道进行双向通信
      • http2 可以头部压缩,能够节省消息头占用的网络的流量,而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源
    • 开启浏览器缓存
      • 强缓存:表示在缓存期间不需要请求,state code 为 200
      • 协商缓存:缓存过期可以使用协商缓存。协商缓存需要请求,如果缓存有效会返回 304,协商缓存需要客户端和服务端共同实现

1.2 FMP(First meaningful paint)

  • 时间段:白屏结束后,在FCP(first contentful paint)和 FMP 之间。反映主要内容出现在页面上所需的时间,也侧面反映了服务器输出任意数据的速度
  • 优化:Skeleton方法事先撑开即将渲染的元素,避免闪屏,同时提示用户。不同框架上都有相应的 Skeleton 实现,如vue-skeleton-webpack-plugin

    1.3 LCP(Largest Contentful Paint)

    最大内容绘制,LCP(Largest Contentful Paint),用于记录视窗内最大的元素绘制的时间,该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。指标变化如下图:
    性能优化 🚁 - 图1
    LCP 其实能比前两个指标更能体现一个页面的性能好坏程度,因为这个指标会持续更新。举个例子:当页面出现骨架屏或者 Loading 动画时 FCP 其实已经被记录下来了,但是此时用户希望看到的内容其实并未呈现,我们更想知道的是页面主要的内容是何时呈现出来的。
    此时 LCP 指标是能够帮助我们实现想要的需求的。
    性能优化 🚁 - 图2
    上图是官方推荐的时间区间,在 2.5 秒内表示体验优秀。

1.4 TTI(Time to Interactive)

  • 时间段:当有意义的内容渲染出来后,用户会尝试与页面交互,看起来页面加载完毕了,实际 JavaScript 脚本依然在密集执行。期间绝大部分的性能消耗都在 JavaScript 的解释和执行上,这时决定 JavaScript 解析速度有两点:JavaScript 脚本体积和本身执行速度
  • 优化
    • JavaScript 脚本体积
      • SplitChunksPlugin拆包
      • Tree Shaking:通过程序流分析找出代码中无用的代码并剔除。使用 ES6 模块来开启
    • JavaScript 本身执行速度
      • 动态加载 ES6 代码:<script type="module">这个标签来判断浏览器是否支持 es6
      • polyfill 动态加载:引入<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>即可,是否需要 polyfill
      • 路由级别code splitting:给 babel 设置plugin-syntax-dynamic-import这个动态 import 的插件

2、组件加载

2.1 组件懒加载

懒加载:即非关键资源(屏幕可视范围外的资源)延迟加载。可降低网页首屏的 HTTP 请求数,提高首屏加载速度。

A、图像懒加载:加载页面时会先加载轻量级的占位符图像,滚动到视口时,将之替换为延迟加载的图像

  • 内联图像:Chrome 75起支持<img>元素loading="lazy"属性。设置data-src属性指向实际需加载的图像,src指向默认的图片
    • 使用Intersection Observer API检查元素的可见性
    • 使用 scroll 和 resize 事件处理程序(兼容性最好)
  • CSS 中的图像:
    • 使用Intersection Observer检查元素可见性
    • 若在可视范围内,则对元素添加visible

B、视频懒加载

  • 视频不自动播放:
    • <video>元素指明preload属性为preload="none"
    • 使用poster属性为<video>元素提供占位符poster="placeholder.jpg"
  • 视频代替动画 GIF:使用Intersection Observer处理
  • 懒加载库:Lozad.js

C、组件懒加载:运用 react 的lazySuspense实现组件懒加载。在服务端渲染中尚不可用

  1. const Lazycomponent1 = React.lazy(() => import("./lazy.component1.js"));
  2. const Lazycomponent2 = React.lazy(() => import("./lazy.component2.js"));
  3. const Lazycomponent3 = React.lazy(() => import("./lazy.component3.js"));
  4. function AppComponent() {
  5. return;
  6. <div>
  7. <Suspense fallback={<div>loading ...</div>}>
  8. <LazyComponent1 />
  9. <LazyComponent2 />
  10. <LazyComponent3 />
  11. </Suspense>
  12. </div>;
  13. }
  14. //引入多个 lazy load 组件,并指定同一个 Suspense fallback 来处理 loading 状态

2.2 组件预加载

预加载(preload): 提供了一种声明式的命令,让浏览器提前加载指定资源(关键的脚本、样式、字体、主要图片等),在需要执行的时候再执行。在页面加载后需要立即使用资源时使用。目前firefoxIE不支持

  • 使用 link 标签:如<link rel="preload" href="/style.css" as="style" />
  • "as"属性帮助浏览器安排优先级,值可为style、script、font、fetch
  • 在使用 preload 获取字体时添加 crossorigin 属性
  • preload告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源

🖕 二、执行篇

1、动画性能优化

1.1 CSS3 动画优化

  • 将动画放在一个独立图层,避免动画效果影响其他渲染层的元素
  • 尽量避免回流和重绘
  • 尽量使用 GPU,速度更快

1.2 Canvas 动画优化

  • 运用requestAnimationFrame
  • 能将所有动画放到一个浏览器重绘周期里去做,保存CPU循环次数
  • 运行时浏览器会自动优化方法的调用,页面非激活状态动画会自动暂停
  • 离屏canvas:把离屏canvas当成一个缓存区。把需重复绘制的画面数据缓存起来,减少调用canvasAPI消耗
  • 避免浮点运算
  • 减少调用Canvas API
  • web workers API:在后台运行脚本的方法,与网页隔离。遇到大规模的计算,可以通过此 API 分担主线程压力

2、大量数据性能优化

2.1 虚拟列表(Virtual List)

虚拟列表指的就是可视区域渲染的列表。实现虚拟列表就是处理滚动条滚动后的可见区域的变更。——处理大量数据的渲染环节

2.2 Web Worker

利用Web Worker进行多线程编程——处理大量的数据计算环节

  • 当主线程在处理界面事件时,worker 可以在后台运行处理大量的数据计算,将计算结果返回给主线程,由主线程更新 DOM 元素
  • 和主线程通过onmessagepostMessage接口进行通信

🛶三、重新渲染篇

React 组件 props 或 state 改变,都会重新渲染。渲染即意味着更新dom,具体 render 时通过 diffing(对比)、reconciliation(协调) 来比较虚拟dom、协调更新必要的dom。然而,当不必要的重新渲染次数过多,对比协调过程可能会拖慢应用,从而出现性能问题。

1、React 函数组件减少不必要的 render 优化

组件每次更新渲染前会调用shouldComponentUpdate()方法。记忆(memoizing)组件(函数组件包裹于React.memo或类组件扩展于React.PureComponent)即是shouldComponentUpdate()方法的一种实现。
React的组件重新渲染(re-render),一般有以下几种情况,优化主要关注第三四种情况:

  • 组件自己状态(state)改变,重新渲染;
  • 组件依赖的上下文(context)改变,重新渲染;
  • 父组件重新渲染,子组件接收父组件的参数(props)变化,子组件重新渲染
  • 父组件重新渲染,子组件接收父组件的参数(props)不变,子组件重新渲染;

1.1 React.memo

如果React.memo包装了一个函数组件,它将记住渲染的输出结果,并在state,props 或 context 未更改的情况下跳过后续的渲染。

  • React.memo只检查 props 的更改。 如果组件的 state 或 context 发生更改,即使 props 没有更改,也将重新渲染组件

    React.memo only checks for prop changes. If your function component wrapped in React.memo has a useState, useReducer or useContext Hook in its implementation, it will still rerender when state or context change.

  • React.memo对 props 进行了浅比较。 如果要控制比较,可提供自定义比较函数作为第二个参数。

  • React.memo仅作为性能优化存在,不应依赖它阻止渲染。使用前后需性能度量确定是否值得增加复杂性。
    • 不建议使用:组件重新渲染不会太影响性能;比较函数执行较昂贵;
    • 建议使用:组件渲染一次消耗很多性能
      1. function MyComponent(props) {
      2. /* render using props */
      3. }
      4. function areEqual(prevProps, nextProps) {
      5. /*
      6. return true if passing nextProps to render would return
      7. the same result as passing prevProps to render,
      8. otherwise return false
      9. */
      10. }
      11. export default React.memo(MyComponent, areEqual);

因此针对组件第四种情况,如果父组件传递的Props不变,但父组件自己需要重新渲染,而子组件渲染一次很昂贵,不需要不必要的重新渲染,可以使用React.memo包裹子组件。

1.2 React.useCallBack

把函数以及依赖项作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新

  1. const memoizedCallback = useCallback(
  2. () => {
  3. doSomething(a, b);
  4. },
  5. [a, b],
  6. );

因此,针对组件第三种情况,父组件将函数作为props传递给子组件时,函数作为一个引用,每一次父组件渲染,子组件接收的函数也会变化,在父组件用useCallBack后可以控制函数是否变更。

1.3 React.useRef

若需在 state 或 context 更新的时候阻止当前组件渲染,证明这个属性不适合作为 state、context,而应该作为静态属性或者放在 class 外面作为一个简单的变量 。「ref」 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

  • ref 的值在组件重新渲染之间保持不变
  • 更新 ref 不会触发组件重新渲染
  • ref 更新必须在useEffect()回调内部或在处理程序(事件处理程序,计时器处理程序等)中进行更新

其与state的不同之处:

  • state状态更新是异步更新的,会 schedule 安排一个状态更新,在组件下一次渲染后拿到最新的值;而ref 更新是同步更新的,能实时拿到最新的值
  • ref 用于存储组件的基础结构数据,而 state 用于存储直接在屏幕上呈现的信息

1.4 React.useReducer

1.5 合理拆分组件

合理拆分组件,可以控制更小粒度的更新。如果整个页面只有一个大的组件,那么当 props 或者 state 变更之后,需要 reconciliation 的是整个组件。

  • 向下移动 state :当一部分的 state 可以单独抽离出去作为另一个子组件时,就不必在父组件设置 state,也就不会影响到父组件中其他子组件的重新渲染。
  • 内容提升:当一部分 state 某子组件依赖,父组件也依赖因而不能单独抽离出去时。不依赖的部分就依然放在另一子组件中,然后以JSX内容的形式传递给父组件,也被称为children属性。

四、参考资料

1、《还在看那些老掉牙的性能优化文章么?这些最新性能指标了解下》
2、《写在 2021 的前端性能优化指南》
3、https://www.webpagetest.org/
4、《Chrome DevTools Performance