16、React 应用性能优化指南

React 应用性能优化指南

在开发 React 应用时,性能优化是一个至关重要的环节。本文将介绍多种优化策略,包括服务器端渲染、摇树优化、按需更新 DOM、使用 CSV 代替 JSON 以及预渲染、预取和预缓存等,帮助你提升应用的性能和用户体验。

服务器端渲染(SSR)

创建 React 应用(CRA)单页应用(SPA)模式在某些情况下表现出色,因为它不会进行页面刷新,让用户感觉像是在使用移动应用。然而,CRA 并不直接支持服务器端渲染(SSR)。虽然可以通过配置路由等方式让 CRA 实现 SSR,但这可能需要自行维护配置,且不一定值得。

如果你需要 SSR 来提升性能,建议使用已经配置好 SSR 的 React 库,如 Next.js 框架、Razzle 或 Gatsby。

摇树优化(Tree Shaking)

摇树优化是 JavaScript 中用于移除死代码的技术。死代码包括两种情况:
- 从未执行的代码:在运行时从未被执行的代码。
- 结果未被使用的代码:代码被执行,但结果从未被使用。

在当前项目配置中,如果使用了 Recoil 状态管理但未实际使用其功能,会增加代码体积。例如,在 src/AppRouter.tsx 中移除 Recoil 引用可以优化代码:

// 优化前
import React, { FunctionComponent, Suspense } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import { RecoilRoot } from 'recoil'
import App from './App'
const AppRouter: FunctionComponent = () => {
  return (
    <Router>
      <RecoilRoot>
        <Suspense fallback={<span>Loading...</span>}>
          <Switch>
            <Route exact path="/" component={App} />
          </Switch>
        </Suspense>
      </RecoilRoot>
    </Router>
  )
}

// 优化后
import React, { FunctionComponent } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import App from './App'
const AppRouter: FunctionComponent = () => {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={App} />
      </Switch>
    </Router>
  )
}

还可以通过 yarn remove recoil 命令从 package.json 文件中移除 Recoil。移除未使用的库导入后,运行 yarn analyzer 可以看到代码体积减小。

按需更新 DOM

在使用 D3 和 React 时,让 React 控制 DOM 是一个很大的优势。但需要确保仅在数据发生变化时重新渲染。D3 代码被视为副作用,因为它在 React 的虚拟 DOM 机制之外向 DOM 添加内容。

HelloD3Data.tsx 组件为例,每次组件更新时都会重新绘制。为了避免不必要的重绘,可以采用以下三种方法:
- 检查 D3 数据 :通过选择所有 p 元素并迭代数组,创建一个先前数据对象进行比较。

import React, { useEffect } from 'react'
import './HelloD3Data.scss'
import { select, selectAll } from 'd3-selection'
interface IHelloD3DataProps {
  data: string[]
}
const HelloD3Data = (props: IHelloD3DataProps) => {
  useEffect(() => {
    draw()
  })
  const draw = () => {
    const previousData: string[] = []
    const p = selectAll('p')
    p.each((d, i) => {
      previousData.push(d as string)
    })
    if ( JSON.stringify(props.data) !== JSON.stringify(previousData) ) {
      console.log('draw!')
      select('.HelloD3Data')
        .selectAll('p')
        .data(props.data)
        .enter()
        .append('p')
        .text((d) => `d3 ${d}`)
    }
  }
  return (
    <div className="HelloD3Data">
    </div>
  )
}
interface IHelloD3DataProps {
  data: string[]
}
export default HelloD3Data
  • 克隆数据 :克隆数据并将 props 值与状态值进行比较。
import React, { RefObject, useEffect, useState } from 'react'
import './HelloD3Data.scss'
import { select } from 'd3-selection'
const ref: RefObject<HTMLDivElement> = React.createRef()
const HelloD3DataCloned = (props: IHelloD3DataProps) => {
  const [data, setData] = useState<string[]>([])
  useEffect(() => {
    if (JSON.stringify(props.data) !== JSON.stringify(data)){
      setData(props.data)
      console.log('draw!')
      select(ref.current)
        .selectAll('p')
        .data(data)
        .enter()
        .append('p')
        .text((d) => `d3 ${d}`)
    }
  }, [data, props.data, setData])
  return <div className="HelloD3Data" ref={ref} />
}
interface IHelloD3DataProps {
  data: string[]
}
export default HelloD3DataCloned
  • 创建 React 类组件 :使用类组件的生命周期钩子(如 componentDidUpdate )来比较先前和当前的 props 数据。
import React, { RefObject } from 'react'
import { select } from 'd3-selection'
export default class HelloD3DataClass extends React.PureComponent<IHelloD3DataClassProps, IHelloD3DataClassState> {
  ref: RefObject<HTMLDivElement>
  constructor(props: IHelloD3DataClassProps) {
    super(props)
    this.ref = React.createRef()
  }
  componentDidMount() {
    this.draw()
  }
  componentDidUpdate(prevProps: IHelloD3DataClassProps, prevState: IHelloD3DataClassState) {
    if (JSON.stringify(prevProps.data) !== JSON.stringify(this.props.data)) {
      this.draw()
    }
  }
  draw = () => {
    console.log('draw!')
    select(this.ref.current)
      .selectAll('p')
      .data(this.props.data)
      .enter()
      .append('p')
      .text((d) => `d3 ${d}`)
  }
  render() {
    return (
      <div className="HelloD3DataClass" ref={this.ref} />
    )
  }
}
interface IHelloD3DataClassProps {
  data: string[]
}
interface IHelloD3DataClassState {
  // TODO
}
使用 CSV 代替 JSON

在创建 D3 图表时,CSV 和 JSON 是常用的数据格式。如果有选择的话,建议使用 CSV,因为它具有以下优点:
- 带宽占用少 :CSV 使用字符分隔符,而 JSON 需要更多字符来表示语法格式。
- 数据处理速度快 :CSV 的字符分隔符更容易拆分,而 JSON 需要解释语法。

优化 CRA 应用

可以通过预渲染、预取和预缓存来优化 CRA 应用。

预渲染

CRA 本身不支持服务器端渲染,但可以进行预渲染。推荐使用 React-snap 进行预渲染,它可以自动创建不同路由的预渲染 HTML 文件。

操作步骤如下:
1. 安装 React-snap:

$ yarn add --dev react-snap
  1. package.json 中添加 post build 脚本:
"scripts": {
  ...
  "postbuild": "react-snap"
}
  1. 为了避免“无样式内容闪烁”(FOUC)问题,可以在 package.json 中启用内联 CSS:
"reactSnap": {
  "inlineCss": true
}
  1. src/index.tsx 中进行水合操作并注册预缓存:
import React from 'react'
import { hydrate, render } from 'react-dom'
import './index.scss'
import AppRouter from './AppRouter'
import * as serviceWorker from './serviceWorker'
const rootElement = document.getElementById('root')
if (rootElement && rootElement!.hasChildNodes()) {
  hydrate(<AppRouter />, rootElement)
  serviceWorker.register()
} else {
  render(<AppRouter />, rootElement)
}
  1. 构建生产版本的应用:
$ yarn build
添加新页面

AppRouter.tsx 中添加新页面路由:

import Rectangle from './components/Rectangle/Rectangle'
const AppRouter: FunctionComponent = () => {
  return (
    <Router>
      <RecoilRoot>
        <Suspense fallback={<span>Loading...</span>}>
          <Switch>
            <Route exact path="/" component={App} />
            <Route exact path="/Rectangle" component={Rectangle} />
          </Switch>
        </Suspense>
      </RecoilRoot>
    </Router>
  )
}

为了让新页面被爬取,需要在 App.tsx 中添加路由链接:

<NavLink to='/Rectangle' key='Rectangle'>
  Navigate To Rectangle
</NavLink>
预取(Prefetching)

可以使用 Quicklink 来优化 JS 包的加载顺序,让页面先加载,然后再获取 JS 包。

操作步骤如下:
1. 安装依赖:

$ yarn add -D webpack-route-manifest
$ yarn add quicklink
  1. src/AppRouter.tsx 中使用 Quicklink HOC 添加预取功能:
import React, { FunctionComponent, lazy, Suspense } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import { RecoilRoot } from 'recoil'
import { withQuicklink } from 'quicklink/dist/react/hoc.js'
import App from './App'
const MyPage = lazy(() => import('./components/Rectangle/Rectangle'))
const options = {
  origins: [],
}
const AppRouter: FunctionComponent = () => {
  return (
    <Router>
      <RecoilRoot>
        <Suspense fallback={<span>Loading...</span>}>
          <Switch>
            <Route exact path="/" component={App} />
            <Route exact path="/Rectangle" component={withQuicklink(MyPage, options)} />
          </Switch>
        </Suspense>
      </RecoilRoot>
    </Router>
  )
}
export default AppRouter

通过以上优化策略,你可以显著提升 React 应用的性能和用户体验。但需要注意的是,预渲染和静态页面服务并不总是最佳选择,需要根据应用的具体情况进行测试和调整。

React 应用性能优化指南

性能优化策略总结

为了更清晰地展示上述各种性能优化策略,下面通过表格进行总结:
| 优化策略 | 具体方法 | 优点 | 操作步骤 |
| — | — | — | — |
| 服务器端渲染(SSR) | 使用 Next.js 框架、Razzle 或 Gatsby | 提升性能 | 选择合适的框架并进行相应配置 |
| 摇树优化(Tree Shaking) | 移除未使用的库和代码 | 减小代码体积 | 移除代码中的引用,使用 yarn remove 移除库,运行 yarn analyzer 检查 |
| 按需更新 DOM | 检查 D3 数据、克隆数据、创建 React 类组件 | 避免不必要的重绘 | 按对应代码示例修改组件 |
| 使用 CSV 代替 JSON | 在创建 D3 图表时优先选择 CSV | 减少带宽占用,加快数据处理速度 | 选择 CSV 作为数据格式 |
| 预渲染 | 使用 React-snap | 自动创建预渲染 HTML 文件 | 安装 React-snap,配置 package.json ,在 src/index.tsx 处理 |
| 预取(Prefetching) | 使用 Quicklink | 优化 JS 包加载顺序 | 安装依赖,在 src/AppRouter.tsx 中使用 Quicklink HOC |

各优化策略的流程图

下面是预渲染和预取优化策略的 mermaid 流程图:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A([开始]):::startend --> B(安装 React-snap):::process
    B --> C(配置 package.json 脚本):::process
    C --> D(启用内联 CSS):::process
    D --> E(在 src/index.tsx 处理):::process
    E --> F(构建生产版本):::process
    F --> G([结束]):::startend

    H([开始]):::startend --> I(安装依赖):::process
    I --> J(在 src/AppRouter.tsx 使用 Quicklink HOC):::process
    J --> K([结束]):::startend
不同优化策略的应用场景分析
  • 服务器端渲染(SSR) :适用于对搜索引擎优化(SEO)要求较高、需要快速加载首屏内容的应用。例如新闻网站、电商平台等,通过 SSR 可以让搜索引擎更好地抓取页面内容,同时提升用户首次访问的加载速度。
  • 摇树优化(Tree Shaking) :在项目中引入了大量第三方库,但实际使用的功能较少时,摇树优化可以有效减小代码体积。比如在一个小型的 React 工具应用中,引入了一个功能丰富的 UI 库,但只使用了其中的几个组件,此时摇树优化就可以移除未使用的代码。
  • 按需更新 DOM :在使用 D3 进行数据可视化的 React 应用中,由于 D3 操作 DOM 可能会导致不必要的重绘,按需更新 DOM 可以避免这种情况。例如实时数据监控图表,数据更新频率较高,通过按需更新可以提高性能。
  • 使用 CSV 代替 JSON :当处理大量数据时,CSV 可以减少带宽占用和提高数据处理速度。比如在大数据分析平台中,从服务器获取大量数据进行图表展示,使用 CSV 可以提升性能。
  • 预渲染 :对于单页应用(SPA),预渲染可以在一定程度上模拟服务器端渲染的效果,提升页面加载速度和 SEO。例如企业官网、博客网站等,通过预渲染可以让搜索引擎更好地抓取页面,同时用户可以更快地看到页面内容。
  • 预取(Prefetching) :适用于页面中包含大量 JS 资源的应用,通过预取可以提前加载后续页面所需的资源,提升用户导航到后续页面的速度。比如大型电商网站的商品列表页和详情页,用户在浏览列表页时可以预取详情页的资源。
注意事项和总结

在进行性能优化时,需要注意以下几点:
- 预渲染和静态页面服务并不总是最佳选择,对于轻量级应用,可能等待所有内容一次性加载完成会有更好的用户体验。需要根据应用的具体情况进行测试和调整。
- 在使用 React-snap 进行预渲染时,可能会出现“无样式内容闪烁”(FOUC)问题,需要通过启用内联 CSS 来解决。
- 在移除未使用的库和代码时,要确保不会影响应用的正常功能,建议在移除前进行充分的测试。

总之,通过合理运用上述性能优化策略,可以显著提升 React 应用的性能和用户体验。在实际开发中,需要根据应用的特点和需求,选择合适的优化策略,并进行充分的测试和调整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值