React 的未来,Suspense,lazy 异步加载组件[性能优化]

1,932 阅读4分钟

Suspense,lazy 是 React 16.x 的新特性 话不多说让我们开始吧

  • 在你的应用中引入代码分割的最佳方式是通过动态 import() 语法。

注意: 动态 import() 语法目前只是一个 ECMAScript (JavaScript) 提案, 而不是正式的语法标准。预计在不远的将来就会被正式接受。

Webpack 解析到该语法时,它会自动地开始进行代码分割。如果你使用 Create React App,该功能已配置好,你能立刻使用 这个特性。Next.js 也已支持该特性而无需再配置。 如果你自己配置 Webpack,你可能要阅读下 Webpack 关于代码分割的指南。你的 Webpack 配置应该类似于此。 当使用 Babel 时,你要确保 Babel 能够解析动态 import 语法而不是将其进行转换。对于这一要求你需要 babel-plugin-syntax-dynamic-import 插件。

React.lazy

注意: React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,我们推荐 Loadable Components 这个库。它有一个很棒的服务端渲染打包指南。

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}

这个函数他返回了一个Promise,需要用函数去调用,而且还是主动抛错的,resolve

  • 这样做就完成了组件的异步加载,但是这样做有个问题那就是: 如果在 Home 渲染完成后,包含 OtherComponent 的模块还没有被加载完成,我们可以使用加载指示器为此组件做优雅降级。这里我们使用 Suspense 组件来解决。
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const TowComponent = React.lazy(() => import('./TowComponent'))

function MyComponent() {
  return (
   <Suspense fallback={<div>Loading...</div>}>
    	<OtherComponent />
   		<TowComponent />
   </Suspense>
  );
}
  • 如果在 MyComponent 渲染完成后,包含 OtherComponent 的模块还没有被加载完成,我们可以使用加载指示器为此组件做优雅降级。这里我们使用 Suspense 组件来解决。
  • Suspense 作用域取件可以接受多个组件

MyErrorBoundary

那么 我们继续问题,懒加载的组件,在加载的过程中遇到一个问题如(网络问题)我们该怎么样来维护用户体验呢?——不至于让用户看到空白 它会触发一个错误。你可以通过异常捕获边界**(Error boundaries)**

注意 错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,这也类似于 JavaScript 中 catch {} 的工作机制。

还是上面代码:

import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const TowComponent = React.lazy(() => import('./TowComponent'))

function MyComponent() {
  return (
  <MyErrorBoundary>
	   <Suspense fallback={<div>Loading...</div>}>
	    	<OtherComponent />
	   		<TowComponent />
	   </Suspense>
  <MyErrorBoundary>
  
  );
}

MyErrorBoundary 如下【选自官方文档】

class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}
export default MyErrorBoundary ;

基于路由代码分割

好让我们回来 在SPA 单页面程序中,我们可能要在多个页面中用到组件的懒加载,这样的话我们就可以吧他定义在路由中 很简单 套进去就可以了

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

命名导出(Named Exports)

接下来我们就会想了,既然是import 那是不是也可以向导入组件那种 进行 重命名 错误示范

const Tow = React.lazy(() => import('./TowComponent'))

注意:不可以直接这样子做 ,eact.lazy 目前只支持默认导出(default exports)。如果你想被引入的模块使用命名导出(named exports),你可以创建一个中间模块,来重新导出为默认模块。这能保证 tree shaking 不会出错,并且不必引入不需要的组件。

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));

但是 真的有必要这样子做吗?