React 中的代码拆分
大家好,我是 Sagar,一名高级软件工程师。我喜欢撰写文章,帮助开发者理解 JavaScript 的魅力。如果您对本文有任何疑问,请留言,我会回复您,或者您也可以在 Twitter 上关注我:@sagar_dev44。
在 JavaScript 生态系统中,包管理器注册表中有很多优秀的库和框架,我们每天都会将它们导入到项目中。在项目刚启动时,这没什么问题,但随着项目规模的扩大,你就会面临很多性能相关的问题。
在本文中,我们将重点关注常见问题,例如捆绑包大小过大导致启动缓慢,并通过在 React 应用程序中简单实现代码拆分来解决它。
捆绑
大多数现代应用程序都使用Webpack或Browserify将代码库“打包”成单个文件。在你的应用程序规模足够小且依赖关系有限之前,打包代码库是一种非常好的方法。但随着代码库的增长,打包文件的大小也会随之增长,然后就会出现一些问题,例如打包文件过大、启动缓慢、热模块替换缓慢等等。
如果您对捆绑的工作原理感到好奇,我强烈建议您阅读 webpack 的官方文档。
代码拆分
处理大捆绑包大小和缓慢启动的完美解决方案是在您的应用程序中实现代码拆分,即将您的代码拆分成更小的块,然后可以按需或并行加载。
最佳做法是将块大小保持在 150KB 以下,这样即使在网络状况较差的情况下,应用程序也能在 3-5 秒内变得更具交互性。
使用Create React App、Next.js或Gatsby创建应用程序的显著好处是,它们提供了开箱即用的代码拆分设置,或者您可以自行设置。
如果您想自己设置代码拆分,请参阅Webpack 文档上的安装和入门指南。
import()
– 动态导入 ES 模块
在应用中引入代码拆分的最佳方式是通过动态 import()。它使我们能够动态加载 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
});
使用动态,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;
我们在这里做了一些事情:
- 该
asyncComponent
函数以getComponent
一个参数作为参数,当被调用时将动态地import()
运行给定的组件。 - 在上
componentWillMount
,我们只需用.then()
方法解决承诺,然后将this.state.Component
状态转变为动态加载的组件。 - 最后,在
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")
);
如果您运行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。
基于组件的代码拆分
正如我们之前看到的,基于路由的代码拆分非常简单,我们将块分解为应用程序路由。
如果您的特定路线过于复杂,大量使用 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;
我们在这里做了一些事情:
- 我们已经创建了一个简单的
<App />
类组件button
。 - 在组件中
<App />
,单击按钮时,我们会动态导入<Greeting/>
组件并存储在内部this.state.Greeting
状态。 - 在 render() 方法中,我们首先
Greeting
从 中解构this.state
并存储在一个Greeting
常量中。之后&&
,我们用逻辑运算符 (AND) 交叉验证它是否为null
值。一旦 Greeting 为真值,我们就可以<Greeting />
直接将组件应用于jsx
。 - 在后台,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.
使用 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;
希望你喜欢这篇文章。如果你对代码分割感兴趣或想进一步探索,我提供了一些很棒的参考资料。
你已经完成了 React 中的代码拆分。现在,是时候开派对了。

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