在JavaScript生态系统中,包管理器注册表上有很好的库和框​​架,在日常生活中我们将它导入到我们的项目中。刚启动项目时没关系,但是一旦项目增长,您就会遇到很多与性能相关的问题。

在本文中,我们将重点关注常见问题,如大型捆绑大小慢启动和解决它只是在React应用程序中实现代码拆分。

捆绑

大多数现代应用程序通过使用WebpackBrowserify “捆绑”到单个文件。捆绑代码库是一种很好的方式,直到您的应用程序非常小而依赖性有限。一旦您的代码库增长,您的捆绑包大小也会增长,然后发生的问题就像一个大的捆绑包大小,慢启动和慢速热模块更换等。

如果您对捆绑如何工作感到好奇,我强烈建议您阅读webpack的官方文档。

代码拆分

处理大包大小和启动缓慢的完美解决方案是在应用程序中实现代码拆分,即将代码拆分为更小的块,然后可以按需加载或并行加载。

最佳做法是将您的块大小保持在150KB以下,以便应用程序在3-5秒内变得更具交互性,即使在糟糕的网络上也是如此。

使用Create React AppNext.jsGatsby创建应用程序的重要好处,因为它们提供开箱即用的代码分割设置,或者您可以自己设置。

如果您想自己设置代码拆分,请参阅Webpack文档中的安装入门指南

import() - 动态导入ES模块

开始在您的应用中引入代码拆分的最佳方法是通过动态导入()。它使我们能够动态加载ES模块。默认情况下,ES模块是完全静态的。您必须在编译时指定导入和导出的内容,并且无法在运行时更改它。

import CONSTANTS from './constants/someFile.js'; // importing CONSTANTS from someFile.js by using es import

ES模块几乎没有限制,例如es模块应该只出现在文件的顶层意味着如果我们提到上面的任何语句es模块导入它会抛出一个错误而另一个是模块路径被修复我们无法计算或动态改变它。

例如,

const double = (x) => x*x;
import CONSTANTS from './constants/someFile.js'; // it will throw an error because we created double function above es import module

另一方面,动态import()es模块克服了es模块的两个限制,并提供了异步模块导入功能。

const modulePath = './someFile.js'; // path of module
// dynamic import() module
import(modulePath).then(module => {
  return module.default; // return default function of es module
});

使用dynamic import()我们可以指定es模块路径,或者我们可以在运行时更改路径并返回一个promise,如果它抛出错误,我们必须在.then()方法或.catch()方法中处理这个promise 。

请注意,动态import()语法是ECMAScript(JavaScript)提议,目前不是语言标准的一部分。预计在不久的将来会被接受。

有两种方法可以在您的应用中实现代码拆分route-basedcomponent-based代码拆分。您必须决定在您的应用程序中引入代码拆分的位置可能有点棘手。

基于路由的代码拆分

开始代码拆分的好地方是app路由。将应用程序分解为每个路由的块,然后在用户导航该路由时加载该块。在引擎盖下,webpack负责创建块并根据需要向用户提供块。

我们必须创建asyncComponent并使用动态import()函数导入所需的组件。

让我们创建一个asyncComponent组件,通过动态import()返回组件的承诺来获取所需的组件。成功解析组件承诺后,它将返回所需的组件。简单来说,动态import()导入组件是异步的。

// filename: asyncComponent.jsx
import React, { Component } from "react";

const asyncComponent = (getComponent) => {
  // return AsyncComponent class component
  return class AsyncComponent extends Component {
    static Component = null;
    state = {
      Component: AsyncComponent.Component // first time similar to static Component = null
    };

    componentWillMount() {
      if (!this.state.Component) {
        // if this.state.Component is true value then getComponent promise resolve with .then() method
        // For simplicity, I haven't caught an error, but you can catch any errors or show loading bar or animation to user etc.
        getComponent().then(({ default: Component }) => {
          AsyncComponent.Component = Component;
          this.setState({ Component }); // update this.state.Component
        });
      }
    }

    render() {
      const { Component } = this.state; // destructing Component from this.state
      if (Component) {
        // if Component is truthy value then return Component with props
        return <Component {...this.props} />;
      }
      return null;
    }
  };
};

export default asyncComponent;

我们在这里做了几件事:

  1. asyncComponent函数getComponent作为一个参数,当被调用时将动态地import()运行给定的组件。
  2. componentWillMount,我们只需使用.then()方法解析promise,然后将this.state.Component状态变为动态加载的组件。
  3. 最后,在render()方法中,我们从返回加载组件this.state.Componentprops

现在,是时候使用了asyncComponent。首先使用react-router-app分离应用程序的路由。

// filename: index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import asyncComponent from "./asyncComponent";

// import components with asyncComponent (indirectly using dynamic import() function)
const App = asyncComponent(() => import("./App"));
const About = asyncComponent(() => import("./About"));
const PageNotFound = asyncComponent(() => import("./PageNotFound"));

ReactDOM.render(
  <Router>
    <Switch>
      <Route path="/" component={App} exact />
      <Route path="/about" component={About} exact />
      <Route component={PageNotFound} />
    </Switch>
  </Router>,
  document.getElementById("root")
);

如果您yarn run build使用创建的应用程序运行Create React App,您将看到我们的应用程序已分成几个块。

# Before implementing code splitting

File sizes after gzip:

  38.35 KB  build/static/js/1.3122c931.chunk.js
  797 B     build/static/js/main.70854436.chunk.js
  763 B     build/static/js/runtime~main.229c360f.js
  511 B     build/static/css/main.a5142c58.chunk.css

# After implementing code splitting

File sizes after gzip:

  38.33 KB  build/static/js/5.51b1e576.chunk.js
  1.42 KB   build/static/js/runtime~main.572d9e91.js
  799 B     build/static/js/main.3dd161f3.chunk.js
  518 B     build/static/js/1.5f724402.chunk.js
  327 B     build/static/css/1.f90c729a.chunk.css
  275 B     build/static/css/main.6a5df30c.chunk.css
  224 B     build/static/js/2.4a4c0b1e.chunk.js
  224 B     build/static/js/3.76306a45.chunk.js

如果您清楚地观察到块大小,除了两个或三个块之外,所有块大小都低于100KB。

不要过度思考asyncComponent编码的东西,我们稍后会介绍一个React-Loadable库,它为我们提供了一个灵活的api来实现代码分割。

基于组件的代码拆分

正如我们之前看到的,基于路由的代码拆分非常简单,我们将块分解为app路由。

如果您的特定路由太复杂,大量使用UI组件,模型,选项卡等,并且块大小变得更大,那么标准块大小就像150KB。在这种情况下,我们必须向前迈进一步,以便在基于组件(也称为基于组件的代码拆分)的组件的基础上拆分代码

// filename: App.jsx
import React, { Component } from "react";
import asyncComponent from "./asyncComponent"; // imported asyncComponent

// simple class based App component
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      Greeting: null // <== initially set to null
    };
  }

  // handle button clicks
  handleButtonClick = () => {
    if (!this.state.Greeting) {
      // load Greeting component with dynamic import
      const Greeting = asyncComponent(() => import("./Greeting"));
      this.setState(prevState => {
        return {
          Greeting
        };
      });
    }
  };

  render() {
    const { Greeting } = this.state; // grab Greeting component from state
    return (
      <React.Fragment>
        <button onClick={this.handleButtonClick}>Click me</button>
        {Greeting && <Greeting message="lorem ipsum dummy message" />}
      </React.Fragment>
    );
  }
}

export default App;

我们在这里做了几件事:

  1. 我们<App />用a 创建了一个简单的类组件button
  2. <App />组件中,点击按钮我们动态导入<Greeting/>组件并存储在内部this.state.Greeting状态。
  3. 在render()方法中,首先我们Greeting从常量中解析this.state并存储Greeting。稍后使用逻辑&&(AND)运算符,我们交叉检查它不是null值。一旦问候是真值,那么我们就<Greeting />直接利用组件jsx
  4. 在场景背后,Webpack为<Greeting />组件创建单独的块,并根据需要为用户提供服务。

React可加载

React Loadable是一个由@jamiebuilds设计的小型库,可以非常轻松地在React应用程序中实现代码拆分。它通过使用动态import()和Webpack 完成代码分割。

React Loadable提供Loadable更高阶的组件,允许您在将任何模块呈现到应用程序之前动态加载它。

使用npm或yarn将反应可加载包安装到您的应用程序中。

yarn add react-loadable # I'm sticking with yarn for this article.

使用React Loadable实现基于路由器的代码拆分

React Loadable非常简单,您不需要制作任何异步组件或不需要编写复杂的设置。只需导入Loadable组件并提供loader

// filename: index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Loadable from 'react-loadable';

const Loading = () => <h1>Loading...</h1>; // loading component

// dynamic loading <App />, <About /> and <PageNotFound /> components
// Loadable is higher order components. it takes loader which dynamic import() of desired component
// and loading which component shows during successfully resolving dyanmic import()
const App = Loadable({
  loader: () => import("./App"),
  loading: Loading
});

const About = Loadable({
  loader: () => import("./About"),
  loading: Loading
});

const PageNotFound = Loadable({
  loader: () => import("./PageNotFound"),
  loading: Loading
});

ReactDOM.render(
  <Router>
    <Switch>
      <Route path="/" component={App} exact />
      <Route path="/about" component={About} exact />
      <Route component={PageNotFound} />
    </Switch>
  </Router>,
  document.getElementById("root")
);

使用React Loadable实现基于组件的代码拆分

基于组件的代码拆分就像我们在上一节中已经看到的一样简单。

import React, { Component } from "react";
import Loadable from "react-loadable";

const Loading = () => <h1>Loading...</h1>; // loading component

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      Greeting: null
    };
  }

  handleButtonClick = () => {
    if (!this.state.Greeting) {
      // load Greeting component with Loadable component
      const Greeting = Loadable({
        loader: () => import("./Greeting"),
        loading: Loading
      });
      this.setState(prevState => {
        return {
          Greeting
        };
      });
    }
  };

  render() {
    const { Greeting } = this.state; // grab Greeting component from state
    return (
      <React.Fragment>
        <button onClick={this.handleButtonClick}>Click me</button>
        {Greeting && <Greeting message="lorem ipsum dummy message" />}
      </React.Fragment>
    );
  }
}

export default App;

我希望你喜欢这篇文章。如果您好奇或想要在代码分割中探索更多,我已经为您提供了很好的参考。


参考文献:

  1. https://reactjs.org/docs/code-splitting.html
  2. https://developers.google.com/web/fundamentals/performance/optimizing-javascript/code-splitting/
  3. https://hackernoon.com/effective-code-splitting-in-react-a-practical-guide-2195359d5d49
  4. https://alligator.io/react/react-loadable/
  5. https://webpack.js.org/guides/code-splitting/