在 React hook 篇章時我們認識了一些避免 re-render 的 hook,像是 useMemo、useCallback,還有 HOC React.memo。
而在這個篇章中會介紹幾個除了 hook 以外,讓 React 效能更好或是避免效能變差的技巧。
React 專案常常都會搭配著 webpack 使用,而透過 webpack 打包 React 專案後產生一個 bundle.js (或者可以自己修改預設名字)的檔案,裡面包含了許多的 js 程式碼。
當一個專案的規模越龐大時,打包出來的 bundle.js 檔案也會越大,載入網站的時間也會變得更久,但實際上 bundle.js 裡面某些的程式碼會在一開始載入就用上嗎?並不見得。所以我們可以靠 Code-Splitting 的其中一個方式: 以 Dynamic Import 的方式在適當的時機才載入需要的程式碼。
要做 Dynamic Import 有好幾種方式,以下一一介紹:
在 React 官網 有個段落提到可以使用 import() 進行 Dynamic Import。
範例:
import('/modules/my-module.js')
.then((module) => {
// Do something with the module.
});
這兩個是 React 提供的 API,可惜 React 官網 提到 SSR 時不能使用它們。
React.lazy 接受一個 callback function 當作參數,當首次渲染元件時才會 import 該元件的 bundle。
使用範例:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
接著 React.lazy 回傳的 lazy 元件要放在 Suspense 元件內部,並且 Suspense 的 props fallback 可以放置一個元件,當 Suspense 元件內部的元件渲染完成前,就先渲染 fallback 內的元件。
如此一來當一個頁面中有其中一個元件要花比較多時間載入時,就可以用 Suspense 包覆,讓它 loading,避免整個頁面都被 block 住。
import React, { lazy, Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
<AnotherComponent />
</Suspense>
</div>
);
}
另外,React.lazy & React.Suspense 也可以搭配 React Router 使用。
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
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>
);
除了上面的兩個方式外,我們也可以使用 React Loadable 套件 這個套件,如果要做 SSR 就可以使用它來做。
react-loadable 接收了一個物件參數,loader 屬性是一個函式,會 Dynamic Import 需要的元件,當 Dynamic Import 元件還沒渲染完成前,就先渲染 loading 屬性的元件。
import Loadable from 'react-loadable';
const Loading = () => {
return <div>Loading......</div>
};
const LoadableComponent = Loadable({
loader: () => import('./my-component'),
loading: Loading,
});
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}
}
如果要隱藏和顯示元件的話可以透過 CSS 的 opacity 去控制,而不是透過 return 不同的元件去做隱藏和顯示。
// 如果 state view 頻繁變更,元件也會頻繁的 mount & unmount
function Component(props) {
const [view, setView] = useState('view1');
return view === 'view1' ? <SomeComponent /> : <AnotherComponent />
}
// 改善
const visibleStyles = { opacity: 1 };
const hiddenStyles = { opacity: 0 };
function Component(props) {
const [view, setView] = useState('view1');
return (
<React.Fragment>
<SomeComponent style={view === 'view1' ? visibleStyles : hiddenStyles}>
<AnotherComponent style={view !== 'view1' ? visibleStyles : hiddenStyles}>
</React.Fragment>
)
}
因為在每次元件 re-render 時,匿名函式都要被重新分配記憶體位置,行內樣式也是一樣的原理。
import React from 'react';
// bad
// 父元件為 class component
class Page extends React.Component {
render() {
return <Button onClick={() => {
this.setState({ isClicked: true })
}} />
}
}
// 父元件為 function component
const Page = () => {
return <Button onClick={() => {
setIsClicked(true)
}} />
}
// good
// 父元件為 class component
class Page extends React.Component {
handleClick = () => {
this.setState({ isClicked: true })
}
render() {
return <Button onClick={this.handleClick} />
}
}
// 父元件為 function component
const Page = () => {
const handleClick = React.useCallback(() => {
setIsClicked(true)
}, [])
return <Button onClick={handleClick} />
}
將 React element 用 useMemo 優化,不然每次 re-render 都會建立新的 element。
// bad
const Page = () => {
return <Button content={<p>Click me!</p>} />
}
// good
const Page = () => {
const content = React.useMemo(() => <p>Click me!</p>, [])
return <Button content={content} />
}
這點還蠻好理解的,若列表裡面的項目有改變,react 會透過 key 的值去檢查哪些項目要做重新渲染。
如果用 map 的 index 當 key,列表的第一個項目被移除,所有列表項目(可以想成陣列裡面的元素)都往前遞補,原本的第二個項目變成新的第一個項目,項目內容當然會改變導致整個列表重新渲染。
6 tips for better React performance
5 Tips to Improve the Performance of Your React Apps