React 中的代码拆分

2025-06-07

React 中的代码拆分

大家好,我是 Sagar,一名高级软件工程师。我喜欢撰写文章,帮助开发者理解 JavaScript 的魅力。如果您对本文有任何疑问,请留言,我会回复您,或者您也可以在 Twitter 上关注我:@sagar_dev44

在 JavaScript 生态系统中,包管理器注册表中有很多优秀的库和框架,我们每天都会将它们导入到项目中。在项目刚启动时,这没什么问题,但随着项目规模的扩大,你就会面临很多性能相关的问题。

在本文中,我们将重点关注常见问题,例如捆绑包大小过大导致启动缓慢,并通过在 React 应用程序中简单实现代码拆分来解决它。

捆绑

大多数现代应用程序都使用WebpackBrowserify将代码库“打包”成单个文件。在你的应用程序规模足够小且依赖关系有限之前,打包代码库是一种非常好的方法。但随着代码库的增长,打包文件的大小也会随之增长,然后就会出现一些问题,例如打包文件过大、启动缓慢、热模块替换缓慢等等。

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

代码拆分

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

最佳做法是将块大小保持在 150KB 以下,这样即使在网络状况较差的情况下,应用程序也能在 3-5 秒内变得更具交互性。

使用Create React AppNext.jsGatsby创建应用程序的显著好处是,它们提供了开箱即用的代码拆分设置,或者您可以自行设置。

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

import()– 动态导入 ES 模块

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

import CONSTANTS from './constants/someFile.js'; // importing CONSTANTS from someFile.js by using es import
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

另一方面,动态 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
});
Enter fullscreen mode Exit fullscreen mode

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

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

在应用中实现代码拆分有两种方式:一种是代码拆分,route-based另一种是component-based代码拆分。你需要决定在应用中的哪个位置引入代码拆分,这可能有点棘手。

基于路由的代码拆分

代码拆分的一个好起点是应用路由。将应用程序分解成每个路由的多个代码块,然后在用户导航到该路由时加载这些代码块。在底层,webpack 负责创建代码块并根据需要将代码块提供给用户。

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

让我们创建一个asyncComponent组件,它通过动态导入所需的组件import()并返回一个组件的 Promise。组件 Promise 成功解析后,它将返回所需的组件。简而言之,动态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;
Enter fullscreen mode Exit fullscreen mode

我们在这里做了一些事情:

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

现在,是时候使用了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")
);
Enter fullscreen mode Exit fullscreen mode

如果您运行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
Enter fullscreen mode Exit fullscreen mode

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

不要过多考虑asyncComponent编码内容,稍后我们将介绍一个React-Loadable库,它为我们提供了实现代码拆分的灵活 API。

基于组件的代码拆分

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

如果您的特定路线过于复杂,大量使用 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;
Enter fullscreen mode Exit fullscreen mode

我们在这里做了一些事情:

  1. 我们已经创建了一个简单的<App />类组件button
  2. 在组件中<App />,单击按钮时,我们会动态导入<Greeting/>组件并存储在内部this.state.Greeting状态。
  3. 在 render() 方法中,我们首先Greeting从 中解构this.state并存储在一个Greeting常量中。之后&&,我们用逻辑运算符 (AND) 交叉验证它是否为null值。一旦 Greeting 为真值,我们就可以<Greeting />直接将组件应用于jsx
  4. 在后台,Webpack 为<Greeting />组件创建单独的块并根据需要为用户提供服务。

反应可加载

React Loadable是一个由@jamiebuilds设计的小型库,它使得在 React 应用中实现代码拆分变得极其简单。它使用动态import()和 Webpack 来实现代码拆分。

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

使用 npm 或 yarn 将 react-loadable 包安装到您的应用程序中。

yarn add react-loadable # I'm sticking with yarn for this article.
Enter fullscreen mode Exit fullscreen mode

使用 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")
);
Enter fullscreen mode Exit fullscreen mode

使用 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;
Enter fullscreen mode Exit fullscreen mode

希望你喜欢这篇文章。如果你对代码分割感兴趣或想进一步探索,我提供了一些很棒的参考资料。

你已经完成了 React 中的代码拆分。现在,是时候开派对了。

聚会时间

参考:

  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/
文章来源:https://dev.to/sagar/increase-user-interactions-by-implementing-code-splitting-in-react-5a1e
PREV
如何创建基于 Web 的终端
NEXT
用不到 15 行代码将你的网站变成跨平台桌面应用 Electron 是什么?为什么要使用 Electron?入门指南 构建应用 Electron 的缺点 使用 Electron 的项目 Smartsapp 感谢阅读