前言
当我们开发React应用时,性能始终是一个重要的考虑因素。随着应用规模的增长,React组件的数量和复杂性也会相应增加,这可能会导致性能问题的出现。在这篇博文中,我们将探讨如何通过一系列的技巧和最佳实践来优化React应用的性能,以确保用户获得更快的加载时间和更流畅的交互体验。
React是一个强大的JavaScript库,它使我们能够轻松构建交互性强的用户界面。然而,使用不当或忽略性能方面的问题可能导致页面加载缓慢、响应迟钝,甚至影响用户的满意度。因此,了解如何优化React应用的性能对于开发者来说是至关重要的。
在本文中,我们将深入研究React性能优化的各个方面,包括组件渲染优化、状态管理、代码拆分、懒加载等关键概念。我们还将介绍一些常见的工具和技术,以帮助您识别和解决性能瓶颈,从而提高应用的效率。
组件渲染优化
使用Pure Components
Pure Components
是 React 中的一种特殊组件,它会自动进行浅比较(shallow comparison)来确定是否需要重新渲染。这可以减少不必要的渲染,提高性能。
ts
import React, { PureComponent } from 'react';
class MyComponent extends PureComponent {
// ...
}
使用React.memo
React.memo
是一个高阶组件,它可以包裹函数式组件,用于记忆组件的渲染结果,只有在其属性发生变化时才重新渲染。
ts
import React from 'react';
const MyComponent = React.memo(function MyComponent(props) {
// ...
});
控制组件重新渲染
-
shouldComponentUpdate
tsclass MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { // 根据新的props和state来决定是否重新渲染 return this.props.someProp !== nextProps.someProp; } render() { // ... } }
-
useMemo
tsimport React, { useMemo } from 'react'; function MyComponent({ someProp }) { const memoizedValue = useMemo(() => computeExpensiveValue(someProp), [someProp]); return <div>{memoizedValue}</div>; }
组件分割
当一个页面太大, 所以逻辑全部写在这个大组件时, 每次页面状态发生变化时, 组件会重现渲染,由于 render 内执行代码逻辑很多.所以会参数很严重的cpu资源占用。
建议把大页面或者代码多大页面拆分组件去写, 拆分组件不是只为了提组件复用性, 减少代码冗余。
合理的拆分,还可以优化页面性能,和提高代码的可维护性。
优化前
ts
function CounterDisplay() {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
<div>
<div>
<div>
<div>SUPER EXPENSIVE RENDERING STUFF</div>
</div>
</div>
</div>
</div>
)
}
优化后
ts
function ExpensiveComponent() {
return (
<div>
<div>
<div>
<div>SUPER EXPENSIVE RENDERING STUFF</div>
</div>
</div>
</div>
)
}
function CounterDisplay() {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
<ExpensiveComponent />
</div>
)
}
使用React Hooks优化组件
使用 useMemo()
优化耗时的操作
优化前
ts
function CounterDisplay(props) {
let counter = Counter.useContainer()
// 每次 `counter` 改变都要重新计算这个值,非常耗时
let expensiveValue = expensiveComputation(props.input)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
优化后
ts
function CounterDisplay(props) {
let counter = Counter.useContainer()
// 仅在输入更改时重新计算这个值
let expensiveValue = useMemo(() => {
return expensiveComputation(props.input)
}, [props.input])
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
使用 React.memo()
、useCallback()
减少重新渲染次数
优化前
ts
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContainer(useCounter)
function CounterDisplay(props) {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
优化后
ts
function useCounter() {
let [count, setCount] = useState(0)
let decrement = useCallback(() => setCount(count - 1), [count])
let increment = useCallback(() => setCount(count + 1), [count])
return { count, decrement, increment }
}
let Counter = createContainer(useCounter)
let CounterDisplayInner = React.memo(props => {
return (
<div>
<button onClick={props.decrement}>-</button>
<p>You clicked {props.count} times</p>
<button onClick={props.increment}>+</button>
</div>
)
})
function CounterDisplay(props) {
let counter = Counter.useContainer()
return <CounterDisplayInner {...counter} />
}
组件懒加载
懒加载(Lazy Loading)是一项用于优化Web应用性能的关键技术之一,它允许将应用的部分代码(通常是组件或模块)推迟到真正需要时再加载。这有助于减少初始加载时间和资源占用,提高应用的响应速度。下面是懒加载的优化原理:
-
初始加载的轻量化: 在应用的初始加载阶段,只加载必需的核心代码,以加速首次页面加载。这通常包括应用的骨架结构、导航和一些基本组件。
-
按需加载: 一旦初始加载完成,懒加载技术允许您动态加载应用的其他部分,例如某个页面的组件或特定功能模块。这些代码块通常被拆分成小块,每个小块都与一个特定的组件或功能相关联。
-
代码分割: 为了实现懒加载,您可以使用Webpack等打包工具的代码分割功能。通过将应用拆分成多个代码块,您可以在需要时单独加载每个代码块,而不必加载整个应用。
-
动态导入: 在React应用中,您可以使用动态导入(dynamic imports)来实现懒加载。这是通过使用
import()
函数来导入组件或模块的方式,使其成为返回一个Promise的异步操作。
下面是一个示例,演示了React中如何使用懒加载来优化组件的加载:
ts
import React, { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
{/* 常规加载的组件 */}
<RegularComponent />
{/* 懒加载的组件 */}
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
在这个示例中,LazyComponent
组件使用lazy
函数进行懒加载,而Suspense
组件用于处理加载过程中的状态。当用户首次访问应用时,只有RegularComponent
会被立即加载,而LazyComponent
会在需要时才异步加载。fallback
属性用于指定在加载期间显示的占位符。
懒加载的优化原理可以总结为:将应用拆分成多个小块代码,根据需要动态加载这些代码块,以提高初始加载速度和减少资源浪费。这是一种强大的性能优化技术,特别适用于大型和复杂的Web应用。
代码分割与首屏优化
js代码分割
js
optimization: {
splitChunks: {
chunks: 'all',
name: false,
maxSize: 200000,
automaticNameDelimiter: '.',
},
runtimeChunk: {
name: 'runtime',
},
},
以下是对Webpack配置文件中代码分割和优化部分的各个配置项的作用的表格说明:
配置项 | 作用 |
---|---|
optimization.splitChunks |
代码分割配置 |
chunks: 'all' |
指定要对所有类型的代码块(包括初始块、按需加载块等)进行代码分割。 |
name: false |
不为生成的拆分块创建自定义名称,使用Webpack的默认命名规则。 |
maxSize: 200000 |
设置每个拆分块的最大大小为200KB,如果模块大小超过这个限制,将尝试拆分为更小的块。 |
automaticNameDelimiter: '.' |
自动名称的分隔符,用于将模块名称与生成的块名称组合。 |
runtimeChunk |
|
name: 'runtime' |
创建一个包含webpack运行时代码的单独块,以避免在每个拆分块中重复包含运行时代码。 |
这些配置项用于优化代码分割,确保仅加载必要的代码,降低初始加载时间,提高应用性能。根据具体项目需求,可以对这些配置项进行调整和自定义,以获得最佳性能表现。
js
output: {
filename: 'static/js/[name].[contenthash:8].js', // 添加 [contenthash:8] 部分
chunkFilename: 'static/js/[name].chunk.[contenthash:8].js', // 添加 [contenthash:8] 部分
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
配置项 | 作用 |
---|---|
filename |
定义输出的主要 JavaScript 文件的名称。通常根据入口点的名称生成文件,放在 dist/static/js 目录下。 |
chunkFilename |
定义异步加载的代码块(chunks)的文件名模板,包括 [name] (代码块名称)和 [id] (代码块唯一标识符)。这些文件通常放在 dist/static/js 目录下。 |
path |
指定输出文件的根目录,通常使用绝对路径,如 path.resolve(__dirname, 'dist') 。 |
publicPath |
定义浏览器中访问这些文件时的公共路径,通常用于指定资源文件的URL前缀,以确保浏览器可以正确加载资源。 |
这些配置项对于定义输出文件的名称、路径和公共访问路径非常重要,特别是在构建Web应用程序时。通过配置这些选项,您可以控制输出文件的生成结构,确保资源文件可以正确加载,并根据具体项目需求自定义文件名称和目录结构。
上述代码中,我们在 filename
和 chunkFilename
中添加了 [contenthash:8]
部分,这将在文件名中包含内容哈希。:8
表示只使用前8位哈希字符,在项目中,可以根据需要选择不同的长度。以下是关于不同哈希类型的表格说明,以便理解它们的原理和用途:
哈希类型 | 原理和用途 |
---|---|
hash |
hash 是Webpack提供的默认哈希类型。它会生成一个相对短的哈希值,不具备版本控制的作用。它会在每次构建时生成相同的哈希,除非文件内容发生变化。不适合用于长期的缓存管理或版本控制。 |
chunkhash |
chunkhash 基于模块的内容生成哈希,因此每个模块的内容都不同,如果模块的内容发生变化,对应的哈希也会更改。适合用于缓存管理,确保只有在相关模块发生变化时才会更新缓存。 |
contenthash |
contenthash 是基于文件内容生成的哈希,适用于长期的缓存管理和版本控制。当文件内容发生变化时,对应的哈希也会随之更改,确保浏览器可以识别并加载新的文件版本。 |
这些不同类型的哈希用于控制输出文件的名称,并根据不同的需求进行缓存管理和版本控制。chunkhash
和 contenthash
特别适用于生产环境,以确保只有在模块或文件内容发生变化时才更新缓存,提高Web应用的性能和稳定性。选择哪种哈希类型取决于您的项目需求和目标。
这样的配置将确保每次文件内容发生变化时,输出文件名都会更改,从而防止浏览器缓存旧版本的文件。这对于有效的缓存管理和版本控制非常有用,特别是在生产环境中。
css分割
对于css,我们可以使用MiniCssExtractPlugin
配置对代码进行分割:
js
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css', // 添加 [contenthash:8] 部分
chunkFilename: 'static/css/[id].[contenthash:8].chunk.css', // 添加 [contenthash:8] 部分
}),
配置项 | 作用 |
---|---|
filename |
定义输出的每个 CSS 文件的名称,通常根据入口点的名称生成文件,放在 static/css 目录下。 [name] 表示 CSS 文件的名称。 |
chunkFilename |
定义按需加载的 CSS 文件的名称,通常用于拆分的 CSS 模块,也放在 static/css 目录下。 [id] 表示 CSS 模块的唯一标识符。 |
这些配置项用于将CSS样式从JavaScript代码中提取出来,并输出到独立的CSS文件中。这样可以更好地管理样式文件,实现缓存和并行加载,提高Web应用的性能。根据具体项目需求,可以自定义这些配置项来满足不同的目录结构和文件命名需求。
结语
在本篇文章中,我们深入探讨了React应用的性能优化,特别关注了代码分割和首屏优化的重要性。我们了解了Webpack配置中的关键部分,包括代码分割的配置,MiniCssExtractPlugin 的设置以及输出文件名的哈希化。
通过使用代码分割,我们可以将应用的代码拆分成多个块,以便在需要时按需加载,从而提高初始加载性能和用户体验。同时,通过哈希化输出文件名,我们可以实现更好的缓存管理和版本控制,确保浏览器始终加载最新的资源文件。
React 18 和Mobx等现代技术的使用使性能优化变得更加容易和强大。最终,优化React应用的性能需要仔细考虑和定制,以满足特定项目的需求。