Fiber 是 React 的一个重大改进,它是 React 16 中引入的一种新的协调引擎,可以更好地支持渐进式渲染、处理大型组件树以及优化代码的可维护性。Fiber 之所以可以成为 React 性能的一个飞跃,主要有以下两个原因:
改进了一些 React 的内部机制,使得 React 应用程序的性能得到了显著改善。具体来说,Fiber 实现了异步渲染和可中断渲染,可以将渲染任务分割成多个小任务,并调度它们的执行顺序,以提高应用程序的响应性和流畅性。此外,Fiber 还支持渐进式渲染,允许 React 逐步地将组件树渲染到屏幕上,从而更快地响应用户输入,并提高应用程序的交互性。
Fiber 还改进了 React 的代码结构和可维护性,使得 React 开发人员可以更轻松地编写和维护 React 应用程序。具体来说,Fiber 引入了一种新的调度算法和组件架构,将 React 应用程序分解成多个小任务和小组件,使得代码更加模块化和可复用,从而提高了代码的可维护性。
综上所述,Fiber 是 React 性能的一个飞跃,它不仅提高了 React 应用程序的性能和交互性,还改进了 React 的代码结构和可维护性,使得 React 开发人员可以更轻松地编写和维护 React 应用程序。
React Fiber 通过引入一种可中断的调度算法来实现更新过程的可控性。这种调度算法允许 React 在运行过程中中断渲染任务,并在后台执行其他任务,以避免阻塞主线程和提高用户体验。
具体来说,React Fiber 通过将更新任务分解成多个小任务(即 Fiber),并为每个任务分配优先级,以决定任务的执行顺序。这样,React 可以在每个小任务之间进行调度,从而实现更新过程的可控性。
React Fiber 的调度算法基于优先级的概念,每个 Fiber 节点都有一个优先级属性,用于判断任务的重要性和紧急程度。React 根据任务的优先级和当前可用的时间片(即主线程空闲时间)来决定哪个任务可以被执行。
在每个任务执行期间,React Fiber 会检查剩余的时间片,如果时间片不够,React 将暂停当前任务,并将控制权交回给主线程。然后,React 会继续执行更高优先级的任务,或者在后台执行其他任务。当下一个时间片可用时,React 会恢复之前被中断的任务,并继续执行。
通过这种方式,React Fiber 可以在更新过程中灵活地控制任务的执行顺序,并根据优先级调整任务的处理,从而实现对渲染过程的可控性。这种可中断的调度算法使得 React 应用程序可以更好地响应用户输入和交互,并提高了应用程序的性能和流畅性。
随着前端应用体积的扩大,资源加载的优化是我们必须要面对的问题,动态代码加载就是其中的一个方案。
webpack 提供了符合 ECMAScript 提案 的 import() 语法 ,让我们来实现动态地加载模块(注:require.ensure 与 import() 均为 webpack 提供的代码动态加载方案,在 webpack 2.x 中,require.ensure 已被 import 取代)。
在 React 16.6 版本中,新增了 React.lazy 函数,它能让你像渲染常规组件一样处理动态引入的组件,配合 webpack 的 Code Splitting ,只有当组件被加载,对应的资源才会导入 ,从而达到懒加载的效果。
在实际的使用中,首先是引入组件方式的变化:
// 不使用 React.lazy
import OtherComponent from './OtherComponent';
// 使用 React.lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'))
React.lazy 接受一个函数作为参数,这个函数需要调用 import() 。它需要返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。
React.lazy 方法返回的是一个 lazy 组件的对象,类型是 react.lazy,并且 lazy 组件具有 _status 属性,与 Promise 类似它具有 Pending、Resolved、Rejected 三个状态,分别代表组件的加载中、已加载、和加载失败三中状态。
需要注意的一点是,React.lazy 需要配合 Suspense 组件一起使用,在 Suspense 组件中渲染 React.lazy 异步加载的组件。如果单独使用 React.lazy,React 会给出错误提示。
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
路由懒加载的原理是使用了 ES6 的动态 import() 语法,该语法可以在运行时动态加载一个模块。当使用 React.lazy() 创建一个懒加载组件时,React 会自动调用 import() 来加载该组件,并将其包装在一个 Promise 中。在使用该组件时,React 将等待 Promise 加载完成,然后渲染该组件。如果加载过程中出现错误,React 将抛出一个异常。
Suspense 内部主要通过捕获组件的状态去判断如何加载,上面我们提到 React.lazy 创建的动态加载组件具有 Pending、Resolved、Rejected 三种状态,当这个组件的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件。
如果遇到网络问题或是组件内部错误,页面的动态资源可能会加载失败,为了优雅降级,可以使用 Error Boundaries 来解决这个问题。
Error Boundaries 是一种组件,如果你在组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 生命周期函数,它就会成为一个 Error Boundaries 的组件。
它的用法也非常的简单,可以直接当作一个组件去使用,如下:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
总结
React.lazy() 和 React.Suspense 的提出为现代 React 应用的性能优化和工程化提供了便捷之路。 React.lazy 可以让我们像渲染常规组件一样处理动态引入的组件,结合 Suspense 可以更优雅地展现组件懒加载的过渡动画以及处理加载异常的场景。
React 有很多性能优化的方法,以下是一些常见的优化技巧:
使用虚拟 DOM:React 使用虚拟 DOM 来管理和更新真实 DOM,通过比较新旧虚拟 DOM 树的差异,只更新差异部分对应的真实 DOM,避免了重复渲染整个组件,提高性能。
使用 key 属性:在列表渲染时,为每个子元素指定唯一的 key 属性,以便 React 可以正确地比较和更新列表中的元素。没有正确使用 key 属性可能会导致不必要的重新渲染,影响性能。
使用 shouldComponentUpdate 或 PureComponent:通过重写 shouldComponentUpdate 方法或使用 PureComponent 组件,可以优化组件的渲染过程。这些方法可以比较组件的前后状态,决定是否进行重新渲染,避免不必要的渲染操作。
使用 React.memo 和 useCallback:React.memo 可以将函数组件进行浅比较,避免不必要的重新渲染。useCallback 可以缓存回调函数,避免在每次渲染时重新创建新的回调函数。
使用异步渲染和 Suspense:React Fiber 架构支持异步渲染和可中断渲染,可以将渲染任务分割成多个小任务,并在控制时间片的情况下执行,提高应用程序的响应性和流畅性。Suspense 可以用来处理组件在异步加载时的加载状态。
使用生命周期方法和钩子函数进行性能优化:合理使用生命周期方法和 React Hooks 可以优化组件的渲染过程,例如使用 componentDidMount 进行异步请求的触发,使用 useEffect 进行副作用操作等等。
使用分割代码和按需加载:使用工具如 Webpack,将代码分割成多个文件,并按需加载,以减少初始加载时间和所需的资源。
使用性能监测工具:使用性能监测工具如 React Profiler、Chrome DevTools 等来检测和分析应用程序的性能瓶颈,并进行相应的优化。
使用虚拟列表虚拟列表是常见的‘长列表’和’复杂组件树’优化方式,它优化的本质就是减少渲染的节点。
虚拟列表只渲染当前视口可见元素。
虚拟列表常用于以下组件场景:
无限滚动列表,grid, 表格,下拉列表,spreadsheets
无限切换的日历或轮播图
大数据量或无限嵌套的树
聊天窗,数据流(feed), 时间轴
等等
总之,以上是一些常见的 React 性能优化方法,根据具体的应用场景和需求,可以选择适合的优化技巧来提升 React 应用程序的性能和用户体验。
随着前端应用体积的扩大,资源加载的优化是我们必须要面对的问题,动态代码加载就是其中的一个方案,webpack 提供了符合 ECMAScript 提案 的 import() 语法 ,让我们来实现动态地加载模块(注:require.ensure 与 import() 均为 webpack 提供的代码动态加载方案,在 webpack 2.x 中,require.ensure 已被 import 取代)。
在 React 16.6 版本中,新增了 React.lazy 函数,它能让你像渲染常规组件一样处理动态引入的组件,配合 webpack 的 Code Splitting ,只有当组件被加载,对应的资源才会导入 ,从而达到懒加载的效果。
在实际的使用中,首先是引入组件方式的变化:
// 不使用 React.lazy
import OtherComponent from './OtherComponent';
// 使用 React.lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'))
React.lazy 接受一个函数作为参数,这个函数需要调用 import() 。它需要返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
如上代码中,通过 import()、React.lazy 和 Suspense 共同一起实现了 React 的懒加载,也就是我们常说了运行时动态加载,即 OtherComponent 组件文件被拆分打包为一个新的包(bundle)文件,并且只会在 OtherComponent 组件渲染时,才会被下载到本地。
需要注意的一点是,React.lazy 需要配合 Suspense 组件一起使用,在 Suspense 组件中渲染 React.lazy 异步加载的组件。如果单独使用 React.lazy,React 会给出错误提示。
Suspense 可以包裹多个动态加载的组件,这也意味着在加载这两个组件的时候只会有一个 loading 层,因为 loading 的实现实际是 Suspense 这个父组件去完成的,当所有的子组件对象都 resolve 后,再去替换所有子组件。这样也就避免了出现多个 loading 的体验问题。所以 loading 一般不会针对某个子组件,而是针对整体的父组件做 loading 处理。
上面使用了 import() 语法,webpack 检测到这种语法会自动代码分割。使用这种动态导入语法代替以前的静态引入,可以让组件在渲染的时候,再去加载组件对应的资源,这个异步加载流程的实现机制是怎么样呢?
import() 原理
import() 函数是由TS39提出的一种动态加载模块的规范实现,其返回是一个 promise。在浏览器宿主环境中一个import()的参考实现如下:
function import(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
script.type = "module";
script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
script.onload = () => {
resolve(window[tempGlobal]);
delete window[tempGlobal];
script.remove();
};
script.onerror = () => {
reject(new Error("Failed to load module script with URL " + url));
delete window[tempGlobal];
script.remove();
};
document.documentElement.appendChild(script);
});
}
结合上面的代码来看,webpack 通过创建 script 标签来实现动态加载的,找出依赖对应的 chunk 信息,然后生成 script 标签来动态加载 chunk,每个 chunk 都有对应的状态:未加载 、 加载中、已加载 。
我们可以运行 React.lazy 代码来具体看看 network 的变化,为了方便辨认 chunk。我们可以在 import 里面加入 webpackChunckName 的注释,来指定包文件名称。
Suspense 内部主要通过捕获组件的状态去判断如何加载, React.lazy 创建的动态加载组件具有 Pending、Resolved、Rejected 三种状态,当这个组件的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件。
如果遇到网络问题或是组件内部错误,页面的动态资源可能会加载失败,为了优雅降级,可以使用 Error Boundaries 来解决这个问题。
Error Boundaries 是一种组件,如果你在组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 生命周期函数,它就会成为一个 Error Boundaries 的组件。
React.lazy() 和 React.Suspense 的提出为现代 React 应用的性能优化和工程化提供了便捷之路。
React.lazy 可以让我们像渲染常规组件一样处理动态引入的组件,结合 Suspense 可以更优雅地展现组件懒加载的过渡动画以及处理加载异常的场景。