Code-Splitting
Bundling
大部分 React 應用程式會使用像是 Webpack、Rollup 或 Browserify 的工具來 bundle 它們的檔案。Bundle 是將 import 的檔案合併為單一的檔案過程:「Bundle」。網頁可以引入 bundle,以一次載入整個應用程式。
Bundle 是將被 import 的檔案合併成一個單一的檔案:「bundle」。這個 bundle 檔案可以被引入到網頁內來載入整個應用程式。
範例
應用程式:
// app.js
import { add } from './math.js';
console.log(add(16, 26)); // 42
// math.js
export function add(a, b) {
return a + b;
}
Bundle:
function add(a, b) {
return a + b;
}
console.log(add(16, 26)); // 42
注意:
你的 bundle 後的最終結果看起來會與此不同。
如果你使用 Create React App、Next.js、Gatsby,或者是類似的工具,會有一個內建的 Webpack 設定來 bundle 你的應用程式。
如果沒有的話,你需要自己設定 bundle。例如,拜訪 Webpack 文件的 Installation 和 Getting Started 指南。
Code-Splitting
Bundle 非常棒,但隨著你的應用程式成長,你的 bundle 也將會隨著增長。特別是你引入了大量的第三方函式庫。 你需要隨時留意 bundle 後的程式碼,這樣你就不會得意外的讓 bundle 檔案變得太大,以至於你的應用程式需要很長的時間才能被載入。
為了避免 bundle 的結果過大,最好的解決問題的方式是開始「split」你的 bundle。 Code-Splitting 是透過由像是 Webpack、 Rollup 和 Browserify(經由 factor-bundle)的 bundler 所支援的功能, 它會建立多個 bundle,可以在 runtime 時動態的被載入。
Code-splitting 可以幫助你「延遲載入」目前使用者所需要的東西,這可以大幅提供你的應用程式效能。雖然你還沒有減少應用程式的程式碼總數量,但你可以避免載入使用者目前使用不到的程式碼,來減少初始載入應用程式的時間。
import()
將 code-splitting 引入到你的應用程式最好的方式是透過動態 import()
語法。
加入前:
import { add } from './math';
console.log(add(16, 26));
加入後:
import("./math").then(math => {
console.log(math.add(16, 26));
});
當 Webpack 遇到這種語法時,它將自動的在你的應用程式啟動 code-splitting。如果你使用 Create React App 的話,它已經幫你設定好了,你可以立即的使用它。在 Next.js 也內建支援這個功能。
如果你是自行設定 Webpack,你可以閱讀 Webpack 的 code-splitting 指南。你的 Webpack 設定看起來應該像這樣。
當使用 Babel 時,你將需要確保 Babel 可以解析動態的 import 語法而不是去轉換它。你可能會需要 @babel/plugin-syntax-dynamic-import。
React.lazy
<<<<<<< HEAD
注意:
React.lazy
和 Suspense 還無法在 server-side render 使用。如果你想要在 server render 應用程式做 code-splitting,我們推薦 Loadable Components。它有一個用於 server-side render 的 bundle splitting 的指南。
React.lazy
讓你 render 一個動態 import 的 component 作為正常的 component。
The React.lazy
function lets you render a dynamic import as a regular component.
707f22d25f5b343a2e5e063877f1fc97cb1f48a1
加入前:
import OtherComponent from './OtherComponent';
加入後:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
當首次 render 這個 component 時,將會自動的載入包含 OtherComponent
的 bundle。
React.lazy
接受一個必須呼叫一個動態 import()
的 function。它必須回傳一個 Promise
,resolve 一個包含 React component 的 default
export 的 module。
lazy component 應在 Suspense
component 內 render,這使我們可以在等待 lazy component 載入時,顯示一些 fallback 內容(像是一個載入的符號)。
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
fallback
prop 接受在等待 component 載入時要 render 的任何 React element。你可以將 Suspense
component 放在 lazy component 上方的任何位置。你甚至可以包覆多個 lazy component 到 Suspense
component 內。
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}
<<<<<<< HEAD
錯誤邊界
=======
Avoiding fallbacks
Any component may suspend as a result of rendering, even components that were already shown to the user. In order for screen content to always be consistent, if an already shown component suspends, React has to hide its tree up to the closest <Suspense>
boundary. However, from the user’s perspective, this can be disorienting.
Consider this tab switcher:
import React, { Suspense } from 'react';
import Tabs from './Tabs';
import Glimmer from './Glimmer';
const Comments = React.lazy(() => import('./Comments'));
const Photos = React.lazy(() => import('./Photos'));
function MyComponent() {
const [tab, setTab] = React.useState('photos');
function handleTabSelect(tab) {
setTab(tab);
};
return (
<div>
<Tabs onTabSelect={handleTabSelect} />
<Suspense fallback={<Glimmer />}>
{tab === 'photos' ? <Photos /> : <Comments />}
</Suspense>
</div>
);
}
In this example, if tab gets changed from 'photos'
to 'comments'
, but Comments
suspends, the user will see a glimmer. This makes sense because the user no longer wants to see Photos
, the Comments
component is not ready to render anything, and React needs to keep the user experience consistent, so it has no choice but to show the Glimmer
above.
However, sometimes this user experience is not desirable. In particular, it is sometimes better to show the “old” UI while the new UI is being prepared. You can use the new startTransition
API to make React do this:
function handleTabSelect(tab) {
startTransition(() => {
setTab(tab);
});
}
Here, you tell React that setting tab to 'comments'
is not an urgent update, but is a transition that may take some time. React will then keep the old UI in place and interactive, and will switch to showing <Comments />
when it is ready. See Transitions for more info.
Error boundaries
707f22d25f5b343a2e5e063877f1fc97cb1f48a1
如果其他的 module 載入失敗(例如,因為網路失敗),它將會觸發一個錯誤。你可以透過錯誤邊界處理這些錯誤來呈現一個好的使用者體驗和管理恢復。一旦你建立了你的錯誤邊界,你可以在任何的 lazy component 上方使用它,當網路發生錯誤時可以顯示一個錯誤狀態。
import React, { Suspense } from 'react';
import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</MyErrorBoundary>
</div>
);
基於 Route 的 Code-Splitting
在你的應用程式決定採用 code-splitting 可能有點棘手。你想要確保選擇的位置可以適當的 split bundle,但不會破壞使用者的體驗。
Route 是一個開始的好地方。Web 上大多數的人都習慣花一些時間來等待頁面的過渡。你也傾向於重新 render 一次整個頁面,所以你的使用者不能同時與頁面上的其他 element 做互動。
這裡是如何在你的應用程式使用像是 React Router 的函式庫與 React.lazy
來設定基於 route 的 code-splitting。
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
Named Exports
React.lazy
目前只支援 default exports。如果你想 import 的 module 使用 named export,你可以建立一個中介 module 來重新 export 它做為預設。這可以確保 tree shaking 不會 pull 無用的 component。
// 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"));