React 性能优化
React 是一个用于构建动态用户界面的强大库,但随着应用程序的增长,可能会出现性能问题。在本指南中,我们将探讨优化 React Web 应用程序的关键步骤,以确保其平稳运行。
1、使用 React 的内置性能工具
React 提供了几个工具来帮助您识别和解决性能瓶颈:
-
React 开发者工具:
使用 Profiler 选项卡来测量组件的渲染情况,并识别不必要的渲染。
-
React 严格模式:
启用严格模式,以便在开发过程中捕获潜在的性能和编码问题。
提示:将您的应用程序包裹在 React.StrictMode
中,以显示有关低效模式的警告。
import Reactfrom'react';
importReactDOMfrom'react-dom/client';
importAppfrom'./App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
2、优化组件渲染
不必要的重新渲染会极大地减慢您的应用程序速度。您可以通过以下方式优化组件渲染:
-
使用
React.memo
:当函数组件的属性未更改时,防止其重新渲染。React 中父组件每次更新都会导致子组件重新渲染,即使子组件的状态没有发生变化。
为了减少重复渲染,我们可以使用 React.memo来缓存组件,这样只有在传入组件的状态值发生变化时才会从新渲染。如果传入的值相同,则会返回缓存的组件。
import React, { memo } from'react';
const MyComponent = ({ data }) => {
console.log('Rendering MyComponent');
return <div>{data}</div>;
};
export default memo(MyComponent);
- 使用
useCallback
和useMemo
:
使用useCallback
来记忆函数;
使用useMemo
来记忆计算开销较大的操作。
import React, { useCallback, useMemo } from'react';
constApp = ({ numbers }) => {
const calculateSum = useMemo(() => numbers.reduce((a, b) => a + b, 0), [numbers]);
const handleClick = useCallback(() =>console.log('Clicked!'), []);
return (
<div>
<h1>Sum: {calculateSum}</h1>
<button onClick={handleClick}>Click Me</button>
</div>
);
};
3、使用 React.lazy 进行代码拆分
使用 React.lazy
和 Suspense
将您的应用程序拆分为较小的包,以减少初始加载时间。
import React, { Suspense } from'react';
constHeavyComponent = React.lazy(() =>import('./HeavyComponent'));
constApp = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
};
exportdefaultApp;
4、避免使用 内联对象
使用内联对象时,react会在每次渲染时重新创建对此对象的引用,这会导致接收此对象的组件将其视为不同的对象。因此,该组件对于props的千层比较始终返回false,导致组件一直渲染。
// Don't do this!
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AComponent style={{ margin: 0 }} aProp={aProp} />
}
// Do this instead :)
const styles = { margin: 0 };
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AComponent style={styles} {...aProp} />
}
5、避免内联函数和匿名函数
内联函数在每次渲染时都会创建新的引用,这可能会导致不必要的重新渲染。相反,在渲染逻辑之外定义函数或使用 useCallback
。
不良实践:
<button onClick={() => console.log('Clicked!')}>Click Me</button>
良好实践:
const handleClick = () => console.log('Clicked!');
<button onClick={handleClick}>Click Me</button>;
6、使用React.PureComponent , shouldComponentUpdate
父组件状态的每次更新,都会导致子组件的重新渲染,即使是传入相同props。但是这里的重新渲染不是说会更新DOM,而是每次都会调用diif算法来判断是否需要更新DOM。这对于大型组件例如组件树来说是非常消耗性能的。
在这里我们就可以使用React.PureComponent , shouldComponentUpdate生命周期来确保只有当组件props状态改变时才会重新渲染
PureComponent 是对类组件的 Props 和 State 进行浅比较;React.memo 是对函数组件的 Props 进行浅比较。
7、使用React.Fragment避免添加额外的DOM
function Component() {
return (
<React.Fragment>
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
</React.Fragment>
)
}
8、减少状态和属性穿透
避免过度的状态更新和不必要的属性穿透:
使用 Context API
或像 Redux
或 Zustand
这样的状态管理库来管理全局状态。
将组件拆分为更小、更专注的组件,这些组件管理自己的状态。
9、优化图像和资源
大型图像和资源可能会减慢您的应用程序速度。以下是优化它们的方法:
使用像 ImageOptim
或 TinyPNG
这样的工具来压缩图像。
使用带有 srcset
属性的响应式图像或像 react-image
这样的库。
使用像 react-lazyload
这样的库来懒加载图像。
10、最小化依赖项
审核您的 package.json
以查找不必要的依赖项。大型依赖包可能会减慢您的应用程序速度。
使用像 Bundlephobia
这样的工具来分析依赖项的大小。
优先选择轻量级的替代方案或自定义解决方案。
额外奖励
使用性能监控工具
利用像 Lighthouse
、Sentry
或 Datadog
这样的工具来监控和调试生产中的性能问题。
结论
总览:react的优化核心思想就是让react跳过重新渲染那个些没有改变的Component,而只重新渲染发生变化的Component。
优化您的 React Web 应用程序是一个持续的过程。首先使用 React 工具识别性能瓶颈,然后逐步应用这些技术。通过遵循这些步骤,您将提供更快、更高效的用户体验。
二、craco/webpack打包优化
1.懒加载图片、css、js,路由组件、antd组件按需加载
2.第三方的包,使用cdn,不要打包进来
3.拆分js
4.避免重复打包依赖
5.开启GZIP压缩
-
安装
craco
:npm install @craco/craco
-
修改
package.json
中的脚本部分,使用craco
代替react-scripts
:"scripts": { "start": "craco start", "build": "craco build", "test": "craco test", "eject": "react-scripts eject" }
-
在项目根目录下创建一个
craco.config.js
文件,用于自定义配置:
const FileManagerPlugin = require("filemanager-webpack-plugin");
const WebpackBar = require("webpackbar");
const { DllReferencePlugin } = require("webpack");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const TerserPlugin = require("terser-webpack-plugin");
const CompressionWebpackPlugin = require("compression-webpack-plugin");
const { addBeforeLoaders, removeLoaders, loaderByName } = require("@craco/craco");
const fs = require("fs");
const path = require("path");
const { name, version } = require("./package.json");
const manifest = require("./public/lib/vendor.json");
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
const appPath = resolveApp(".");
const appBuild = resolveApp("build");
const smp = new SpeedMeasurePlugin();
process.env.PORT = 3000;
// 环境信息
const env = process.env.REACT_APP_TARGET_ENV;
let source = `${appPath}/config.dev.js`;
if (env === "test") {
source = `${appPath}/config.test.js`;
} else if (env === "pre") {
source = `${appPath}/config.pre.js`;
} else if (env === "pro") {
source = `${appPath}/config.pro.js`;
}
module.exports = {
reactScriptsVersion: "react-scripts" /* (default value) */,
babel: {
plugins: [
// lodash按需加载
"lodash",
],
loaderOptions: {
// babel-loader开启缓存
cacheDirectory: true,
},
},
plugins: [
{
plugin: {
overrideDevServerConfig: ({ devServerConfig }) => {
return {
...devServerConfig,
headers: {
"Access-Control-Allow-Origin": "*",
},
};
},
overrideWebpackConfig: ({ webpackConfig, context: { env } }) => {
if (env !== "development") {
// 缩小生产环境所有loaders的检索范围
webpackConfig.module.rules[0].oneOf.forEach((rule) => {
rule.include = path.resolve(__dirname, "src");
});
} else {
// 缩小本地开发环境所有loaders的检索范围
webpackConfig.module.rules[0].include = path.resolve(__dirname, "src");
webpackConfig.module.rules[1].oneOf.forEach((rule, index) => {
rule.include = path.resolve(__dirname, "src");
// 本地开发环境babel-loader比较耗时,故加上thread-loader
if (index === 3) {
const babelLoader = {
loader: rule.loader,
options: rule.options,
};
rule.use = ["thread-loader", babelLoader];
delete rule.loader;
delete rule.options;
}
});
}
return {
...webpackConfig,
};
},
},
},
],
webpack: smp.wrap({
alias: {
"@": path.resolve(__dirname, "src"),
"@components": path.resolve(__dirname, "src/components"),
"@containers": path.resolve(__dirname, "src/containers"),
"@constants": path.resolve(__dirname, "src/constants"),
"@utils": path.resolve(__dirname, "src/utils"),
"@routes": path.resolve(__dirname, "src/routes"),
"@assets": path.resolve(__dirname, "src/assets"),
"@styles": path.resolve(__dirname, "src/styles"),
"@services": path.resolve(__dirname, "src/services"),
"@mocks": path.resolve(__dirname, "src/mocks"),
"@hooks": path.resolve(__dirname, "src/hooks"),
"@stories": path.resolve(__dirname, "src/stories"),
},
// configure: { /* Any webpack configuration options: https://webpack.js.org/configuration */ },
configure: (webpackConfig, { env }) => {
// 配置扩展扩展名优化
webpackConfig.resolve.extensions = [".tsx", ".ts", ".jsx", ".js", ".scss", ".css", ".json"];
// 作为子应用接入微前端的打包适配,不接入微前端可以不需要
webpackConfig.output.library = `${name}-[name]`;
webpackConfig.output.libraryTarget = "umd";
webpackConfig.output.globalObject = "window";
// splitChunks打包优化
webpackConfig.optimization.splitChunks = {
...webpackConfig.optimization.splitChunks,
cacheGroups: {
commons: {
chunks: "all",
// 将两个以上的chunk所共享的模块打包至commons组。
minChunks: 2,
name: "commons",
priority: 80,
},
},
};
// 开启持久化缓存
webpackConfig.cache.type = "filesystem";
// 生产环境打包优化
if (env !== "development") {
webpackConfig.plugins = webpackConfig.plugins.concat(
new FileManagerPlugin({
events: {
onEnd: {
mkdir: [`zip/${name}/dist`, `zip/${name}/template`],
copy: [
{
source: source,
destination: `${appBuild}/config.js`,
},
{
source: `${path.resolve("build")}`,
destination: `zip/${name}/dist`,
},
{
source: path.resolve("template"),
destination: `zip/${name}/template`,
},
],
archive: [
{
source: `zip`,
destination: path.relative(__dirname, `./${name}-${version}-SNAPSHOT.tar.gz`),
format: "tar",
options: {
gzip: true,
gzipOptions: {
level: 1,
},
globOptions: {
nomount: true,
},
},
},
],
delete: ["zip"],
},
},
runTasksInSeries: true,
}),
new BundleAnalyzerPlugin({
analyzerMode: "server",
analyzerHost: "127.0.0.1",
analyzerPort: 8889,
openAnalyzer: true, // 构建完打开浏览器
reportFilename: path.resolve(__dirname, `analyzer/index.html`),
}),
new CompressionWebpackPlugin({
test: /\.(js|ts|jsx|tsx|css|scss)$/, //匹配要压缩的文件
algorithm: "gzip",
}),
);
webpackConfig.optimization.minimizer = [
new TerserPlugin({
parallel: true, //开启并行压缩,可以加快构建速度
}),
];
// 生产环境关闭source-map
webpackConfig.devtool = false;
// 生产环境移除source-map-loader
removeLoaders(webpackConfig, loaderByName("source-map-loader"));
} else {
addBeforeLoaders(webpackConfig, loaderByName("style-loader"), "thread-loader");
addBeforeLoaders(webpackConfig, loaderByName("style-loader"), "cache-loader");
}
return webpackConfig;
},
plugins: [new WebpackBar(), new DllReferencePlugin({ manifest })],
}),
};