原文:21 Performance Optimization Techniques for React Apps
作者:Nishant
譯者:博軒
介紹
在 React
內(nèi)部,React
會使用幾項巧妙的小技術(shù)冗酿,來優(yōu)化計算更新 UI
時拂檩,所需要的最少的更新 DOM
的操作梯码。在大多數(shù)情況下为狸,即使你沒有針對性能進行專項優(yōu)化舆驶,React
依然很快乱陡,但是仍有一些方法可以加速 React
應用程序虽缕。本文將介紹一些可用于改進 React
代碼的有效技巧。
1.使用不可變數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)不變性不是一種架構(gòu)或者設(shè)計模式女淑,它是一種編程思想瞭郑。它會強制您考慮如何構(gòu)建應用程序的數(shù)據(jù)流。在我看來鸭你,數(shù)據(jù)不變性是一種符合嚴格單項數(shù)據(jù)流的實踐屈张。
數(shù)據(jù)不變性,這一來自函數(shù)式編程的概念袱巨,可應用于前端應用程序的設(shè)計阁谆。它會帶來很多好處,例如:
- 零副作用
- 不可變的數(shù)據(jù)對象更易于創(chuàng)建瓣窄,測試,和使用纳鼎;
- 利于解耦俺夕;
- 更加利于追蹤變化裳凸;
在 React
環(huán)境中,我們使用 Component
的概念來維護組件內(nèi)部的狀態(tài)劝贸,對狀態(tài)的更改可以導致組建的重新渲染姨谷。
React
構(gòu)建并在內(nèi)部維護呈現(xiàn)的UI(Virtual DOM)。當組件的 props
或者 state
發(fā)生改變時映九,React
會將新返回的元素與先前呈現(xiàn)的元素進行比較梦湘。當兩者不相等時,React
將更新 DOM件甥。因此捌议,在改變狀態(tài)時,我們必須要小心引有。
讓我們考慮一個用戶列表組件:
state = {
users: []
}
addNewUser = () =>{
/**
* OfCourse not correct way to insert
* new user in user list
*/
const users = this.state.users;
users.push({
userName: "robin",
email: "email@email.com"
});
this.setState({users: users});
}
這里的關(guān)注點是瓣颅,我們正在將新的用戶添加到變量 users
,這里它對應的引用是 this.state.users
譬正。
專業(yè)提示 : 應該將 React
的狀態(tài)視為不可變宫补。我們不應該直接修改 this.state
,因為 setState()
之后的調(diào)用可能會覆蓋你之前的修改曾我。
那么粉怕,如果我們直接修改 state
會產(chǎn)生什么問題呢?比方說抒巢,我們添加 shouldComponentUpdate
贫贝,并對比 nextState
和 this.state
來確保只有當數(shù)據(jù)改變時,才會重新渲染組件虐秦。
shouldComponentUpdate(nextProps, nextState) {
if (this.state.users !== nextState.users) {
return true;
}
return false;
}
即使用戶的數(shù)組發(fā)生了改變平酿,React
也不會重新渲染UI了,因為他們的引用是相同的悦陋。
避免此類問題最簡單的方法蜈彼,就是避免直接修改 props
和 state
。所以俺驶,我們可以使用 concat
來重寫 addNewUser
方法:
addNewUser = () => {
this.setState(state => ({
users: state.users.concat({
timeStamp: new Date(),
userName: "robin",
email: "email@email.com"
})
}));
};
為了處理 React
組件中 props
或者 state
的改變幸逆,我們可以考慮一下幾種處理不可變的方法:
- 對于數(shù)組:使用
[].concat
或es6的[ ...params]
- 對象:使用
Object.assign({}, ...)
或 es6的{...params}
在向代碼庫引入不變性時,這兩種方法有很長的路要走暮现。
但是还绘,最好使用一個提供不可變數(shù)據(jù)結(jié)構(gòu)的優(yōu)化庫。以下是您可以使用的一些庫:
- Immutability Helper:這是一個很好的庫栖袋,他可以在不改變源的情況下拍顷,提供修改后的數(shù)據(jù)。
- Immutable.js :這是我最喜歡的庫塘幅,因為它提供了許多持久不可變的數(shù)據(jù)昔案,包括:List尿贫,Stack,Map踏揣,OrderedMap庆亡,Set,OrderedSet 和 Record捞稿。
-
Seamless-immutable:一個用于提供不可變
JavaScript
數(shù)據(jù)結(jié)構(gòu)的庫又谋,他與普通的數(shù)組和對象向后兼容。 - React-copy-write:一個不可變的React狀態(tài)管理庫娱局,帶有一個簡單的可變API彰亥,memoized選擇器和結(jié)構(gòu)共享。
專業(yè)提示: React setState
方法是異步的铃辖。這意味著剩愧,setState()
方法會創(chuàng)建一個帶轉(zhuǎn)換的 state
, 而不是立即修改 this.state
娇斩。如果在調(diào)用setState()
方法之后去訪問 this.state
仁卷,則可能會返回現(xiàn)有值。為防止這種情況犬第,請setState
在調(diào)用完成后使用回調(diào)函數(shù)運行代碼锦积。
其他資源:
2.函數(shù)/無狀態(tài)組件和 React.PureComponent
在 React
中歉嗓,函數(shù)組件和 PureComponent
提供了兩種不同級別的組件優(yōu)化方案丰介。
函數(shù)組件防止了構(gòu)造類實例,
同時函數(shù)組件可以減少整體包的大小鉴分,因為它比類組件的的體積更小哮幢。
另一方面,為了優(yōu)化UI更新志珍,我們可以考慮將函數(shù)組件轉(zhuǎn)換為 PureComponent
類組件(或使用自定義 shouldComponentUpdate
方法的類)橙垢。但是,如果組件不使用狀態(tài)和其他生命周期方法伦糯,為了達到更快的的更新柜某,首次渲染相比函數(shù)組件會更加復雜一些。
譯注:函數(shù)組件也可以做純組件的優(yōu)化:React.memo(...) 是 React v16.6 中引入的新功能敛纲。 它與 React.PureComponent 類似喂击,它有助于控制 函數(shù)組件 的重新渲染。 React.memo(...) 對應的是函數(shù)組件淤翔,React.PureComponent 對應的是類組件翰绊。React Hooks 也提供了許多處理這種情況的方法:useCallback, useMemo。推薦兩個延伸閱讀:
A Closer Look at React Memoize Hooks: useRef, useCallback, and useMemo ,React Hooks API ? 不只是 useState 或 useEffect
我們應該何時使用 React.PureComponent
监嗜?
React.PureComponent
對狀態(tài)變化進行淺層比較(shallow comparison)琳要。這意味著它在比較時,會比較原始數(shù)據(jù)類型的值秤茅,并比較對象的引用。因此童叠,我們必須確保在使用 React.PureComponent
時符合兩個標準:
- 組件
State
/Props
是一個不可變對象; -
State
/Props
不應該有多級嵌套對象框喳。
專業(yè)提示: 所有使用 React.PureComponent
的子組件,也應該是純組件或函數(shù)組件厦坛。
3.生成多個塊文件
Multiple Chunk Files
您的應用程序始終以一些組件開始五垮。您開始添加新功能和依賴項,最終您會得到一個巨大的生產(chǎn)文件杜秸。
您可以考慮通過利用 CommonsChunkPlugin for webpack 將供應商或第三方庫代碼與應用程序代碼分開放仗,生成兩個單獨的文件。你最終會得到 vendor.bundle.js
和 app.bundle.js
撬碟。通過拆分文件诞挨,您的瀏覽器會緩存較少的資源,同時并行下載資源以減少等待的加載時間呢蛤。
注意: 如果您使用的是最新版本的webpack惶傻,還可以考慮使用 SplitChunksPlugin
4.在 Webpack 中使用 Production
標識生產(chǎn)環(huán)境
如果您使用 webpack 4
作為應用程序的模塊捆綁程序,則可以考慮將 mode
選項設(shè)置為 production
其障。這基本上告訴 webpack
使用內(nèi)置優(yōu)化:
module.exports = {
mode: 'production'
};
或者银室,您可以將其作為 CLI
參數(shù)傳遞:
webpack --mode=production
這樣做會限制對庫的優(yōu)化,例如縮小或刪除開發(fā)代碼励翼。它不會公開源代碼蜈敢,文件路徑等等。
5.依賴優(yōu)化
在考慮優(yōu)化程序包大小的時候汽抚,檢查您的依賴項中實際有多少代碼被使用了抓狭,會很有價值。例如殊橙,如果您使用 Moment.js
會包含本地化文件的多語言支持辐宾。如果您不需要支持多種語言,那么您可以考慮使用 moment-locales-webpack-plugin 來刪除不需要的語言環(huán)境膨蛮。
另一個例子是使用 lodash
叠纹。假設(shè)你使用了 100 多種方法的 20 種,那么你最終打包時其他額外的方法都是不需要的敞葛。因此誉察,可以使用 lodash-webpack-plugin 來刪除未使用的函數(shù)。
以下是一些使用 Webpack
打包時可選的依賴項優(yōu)化列表
6. React.Fragments
用于避免額外的 HTML 元素包裹
React.fragments
允許您在不添加額外節(jié)點的情況下對子列表進行分組惹谐。
class Comments extends React.PureComponent{
render() {
return (
<React.Fragment>
<h1>Comment Title</h1>
<p>comments</p>
<p>comment time</p>
</React.Fragment>
);
}
}
等等持偏!我們還可以使用更加簡潔的語法代替 React.fragments
:
class Comments extends React.PureComponent{
render() {
return (
<>
<h1>Comment Title</h1>
<p>comments</p>
<p>comment time</p>
</>
);
}
}
7.避免在渲染函數(shù)中使用內(nèi)聯(lián)函數(shù)定義
由于在 JavaScript
中函數(shù)就是對象({} !== {})驼卖,因此當 React
進行差異檢查時,內(nèi)聯(lián)函數(shù)將始終使 prop diff
失敗鸿秆。此外酌畜,如果在JSX屬性中使用箭頭函數(shù),它將在每次渲染時創(chuàng)建新的函數(shù)實例卿叽。這可能會為垃圾收集器帶來很多工作桥胞。
default class CommentList extends React.Component {
state = {
comments: [],
selectedCommentId: null
}
render(){
const { comments } = this.state;
return (
comments.map((comment)=>{
return <Comment onClick={(e)=>{
this.setState({selectedCommentId:comment.commentId})
}} comment={comment} key={comment.id}/>
})
)
}
}
您可以定義箭頭函數(shù),而不是為 props
定義內(nèi)聯(lián)函數(shù)考婴。
default class CommentList extends React.Component {
state = {
comments: [],
selectedCommentId: null
}
onCommentClick = (commentId)=>{
this.setState({selectedCommentId:commentId})
}
render(){
const { comments } = this.state;
return (
comments.map((comment)=>{
return <Comment onClick={this.onCommentClick}
comment={comment} key={comment.id}/>
})
)
}
}
8. JavaScript
中事件的防抖和節(jié)流
事件觸發(fā)率代表事件處理程序在給定時間內(nèi)調(diào)用的次數(shù)贩虾。
通常,與滾動和鼠標懸停相比沥阱,鼠標點擊具有較低的事件觸發(fā)率缎罢。較高的事件觸發(fā)率有時會使應用程序崩潰,但可以對其進行控制考杉。
我們來討論一些技巧策精。
首先,明確事件處理會帶來一些昂貴的操作崇棠。例如蛮寂,執(zhí)行UI更新,處理大量數(shù)據(jù)或執(zhí)行計算昂貴任務(wù)的XHR請求或DOM操作易茬。在這些情況下酬蹋,防抖和節(jié)流技術(shù)可以成為救世主,而不會對事件監(jiān)聽器進行任何更改抽莱。
節(jié)流
簡而言之范抓,節(jié)流意味著延遲功能執(zhí)行。因此食铐,不是立即執(zhí)行事件處理程序/函數(shù)匕垫,而是在觸發(fā)事件時添加幾毫秒的延遲。例如虐呻,這可以在實現(xiàn)無限滾動時使用象泵。您可以延遲 XHR
調(diào)用,而不是在用戶滾動時獲取下一個結(jié)果集斟叼。
另一個很好的例子是基于 Ajax
的即時搜索偶惠。您可能不希望每次按鍵時,都會請求服務(wù)器獲取新的數(shù)據(jù)朗涩,因此最好節(jié)流直到輸入字段處于休眠狀態(tài)幾毫秒之后忽孽,再請求數(shù)據(jù)。
節(jié)流可以通過多種方式實現(xiàn)。您可以限制觸發(fā)的事件的次數(shù)或延遲正在執(zhí)行的事件來限制程序執(zhí)行一些昂貴的操作兄一。
防抖
與節(jié)流不同厘线,防抖是一種防止事件觸發(fā)器過于頻繁觸發(fā)的技術(shù)。如果您正在使用 lodash
出革,則可以使用 lodash’s debounce function
來包裝你的方法造壮。
這是一個搜索評論的演示代碼:
import debouce from 'lodash.debounce';
class SearchComments extends React.Component {
constructor(props) {
super(props);
this.state = { searchQuery: “” };
}
setSearchQuery = debounce(e => {
this.setState({ searchQuery: e.target.value });
// Fire API call or Comments manipulation on client end side
}, 1000);
render() {
return (
<div>
<h1>Search Comments</h1>
<input type="text" onChange={this.setSearchQuery} />
</div>
);
}
}
如果您不使用 lodash
,可以使用簡單版的防抖函數(shù)骂束。
function debounce(a,b,c){var d,e;return function(){function h(){d=null,c||(e=a.apply(f,g))}varf=this,g=arguments;return clearTimeout(d),d=setTimeout(h,b),c&&!d&&(e=a.apply(f,g)),e}}
9.避免在 map
方法中使用 Index
作為組件的 Key
在渲染列表時费薄,您經(jīng)常會看到索引被用作鍵。
{
comments.map((comment, index) => {
<Comment
{..comment}
key={index} />
})
}
但是使用 index
作為 key
栖雾, 被用在React虛擬DOM元素
的時候,會使你的應用可能出現(xiàn)錯誤的數(shù)據(jù) 伟众。當您從列表中添加或刪除元素時析藕,如果該 key
與以前相同,則 React虛擬DOM元素
表示相同的組件凳厢。
始終建議使用唯一屬性作為 key
账胧,或者如果您的數(shù)據(jù)沒有任何唯一屬性,那么您可以考慮使用shortid module
生成唯一 key
的屬性先紫。
import shortid from "shortid";
{
comments.map((comment, index) => {
<Comment
{..comment}
key={shortid.generate()} />
})
}
但是治泥,如果數(shù)據(jù)具有唯一屬性(例如ID),則最好使用該屬性遮精。
{
comments.map((comment, index) => {
<Comment
{..comment}
key={comment.id} />
})
}
在某些情況下居夹,將 index
用作 key
是完全可以的,但僅限于以下條件成立時:
- 列表和子元素是靜態(tài)的
- 列表中的子元素沒有ID本冲,列表永遠不會被重新排序或過濾
- 列表是不可變的
10.避免使用 props
來初始化 state
(直接賦值)
我們經(jīng)常需要將帶有 props
的初始數(shù)據(jù)傳遞給 React組件
來設(shè)置初始狀態(tài)值准脂。
考慮一下這段代碼:
class EditPanelComponent extends Component {
constructor(props){
super(props);
this.state ={
isEditMode: false,
applyCoupon: props.applyCoupon
}
}
render(){
return <div>
{this.state.applyCoupon &&
<>Enter Coupon: <Input/></>}
</div>
}
}
片段中的一切看起來都不錯,對吧檬洞?
但是 props.applyCoupon
變化會發(fā)生什么狸膏?它會映射到 state
中嘛? 如果在沒有刷新組件的情況下添怔,props
中的值被修改了湾戳,props
中的值,將永遠不會分配給 state
中的 applyCoupon
广料。這是因為構(gòu)造函數(shù)僅在EditPanel
組件首次創(chuàng)建時被調(diào)用砾脑。
引用React文檔:
避免將 props 的值復制給 state渴析!這是一個常見的錯誤:
constructor(props) {
super(props);
// 不要這樣做
this.state = { color: props.color };
}
如此做毫無必要(你可以直接使用 this.props.color)奸鬓,同時還產(chǎn)生了 bug(更新 prop 中的 color 時,并不會影響 state)旷祸。**只有在你刻意忽略
props
更新的情況下使用。此時汹族,應將props
重命名為initialColor
或defaultColor
萧求。必要時,你可以修改它的key
顶瞒,以強制“重置”其內(nèi)部state
夸政。請參閱博文:你可能不需要派生 state
解決方法:
- 直接使用
props
中的屬性
class EditPanelComponent extends Component {
render(){
return <div>{this.props.applyCoupon &&
<>Enter Coupon:<Input/></>}</div>
}
}
- 當
props
改變時,您可以使用componentDidUpdate
函數(shù)來更新state
榴徐。
class EditPanelComponent extends Component {
constructor(props){
super(props);
this.state ={
applyCoupon: props.applyCoupon
}
}
// reset state if the seeded prop is updated
componentDidUpdate(prevProps){
if (prevProps.applyCoupon !== this.props.applyCoupon) {
this.setState({ applyCoupon: this.props.applyCoupon })
// or do something
}
}
render(){
return <div>{this.state.applyCoupon &&
<>Enter Coupon: <Input/></>}</div>
}
}
11.在 DOM 元素上傳遞 Props
您應該避免將屬性傳播到 DOM
元素中守问,因為它會添加未知的 HTML
屬性,這是不必要的坑资,也是一種不好的做法耗帕。
const CommentsText = props => {
return (
<div {...props}>
{props.text}
</div>
);
};
您可以設(shè)置特定屬性,而不是直接傳遞 Props:
const CommentsText = props => {
return (
<div specificAttr={props.specificAttr}>
{props.text}
</div>
);
};
12.在使用 Redux Connect 時袱贮,同時使用 Reselect 來避免組件的頻繁重新渲染
Reselect
是 Redux
的一個簡單的選擇器庫仿便,可用于構(gòu)建記憶選擇器。您可以將選擇器定義為函數(shù)攒巍,為 React
組件檢索 Redux
狀態(tài)片段嗽仪。
const App = ({ comments, socialDetails }) => (
<div>
<CommentsContainer data={comments} />
<ShareContainer socialDetails={socialDetails} />
</div>
);
const addStaticPath = social => ({
iconPath: `../../image/${social.iconPath}`
});
App = connect(state => {
return {
comments: state.comments,
socialDetails: addStaticPath(state.social)
};
})(App);
在這段代碼中,每次評論數(shù)據(jù)在 state
中 變化時柒莉,CommentsContainer
和 ShareContainer
將會重新渲染闻坚。即使 addStaticPath
不進行任何數(shù)據(jù)更改也會發(fā)生這種情況,因為 socialDetails
由 addStaticPath
函數(shù)返回的的數(shù)據(jù)每次都是一個新的對象 (請記住{} != {})【ばⅲ現(xiàn)在窿凤,如果我們用 Reselect
重寫 addStaticPath
,問題就會消失跨蟹,因為Reselect
將返回最后一個函數(shù)結(jié)果卷玉,直到它傳遞新的輸入。
import { createSelector } from "reselect";
const socialPathSelector = state => state.social;
const addStaticPath = createSelector(
socialPathSelector,
social => ({
iconPath: `../../image/${social.iconPath}`
})
);
14. 記憶化的 React 組件
Memoization是一種用于優(yōu)化程序速度的技術(shù)喷市,主要通過存儲復雜函數(shù)的計算結(jié)果相种,當再次出現(xiàn)相同的輸入,直接從緩存中返回結(jié)果品姓。memoized
函數(shù)通常更快寝并,因為如果使用與前一個函數(shù)相同的值調(diào)用函數(shù),則不會執(zhí)行函數(shù)邏輯腹备,而是從緩存中獲取結(jié)果衬潦。
讓我們考慮下面簡單的無狀態(tài)UserDetails
組件。
const UserDetails = ({user, onEdit}) => {
const {title, full_name, profile_img} = user;
return (
<div className="user-detail-wrapper">
<img src={profile_img} />
<h4>{full_name}</h4>
<p>{title}</p>
</div>
)
}
在這里植酥,所有的孩子 UserDetails
都基于 props
镀岛。只要 props
發(fā)生變化弦牡,這個無狀態(tài)組件就會重新渲染。如果 UserDetails
組件屬性不太可能改變漂羊,那么它是使用組件的 memoize
版本的一個很好的選擇:
const UserDetails = ({user, onEdit}) =>{
const {title, full_name, profile_img} = user;
return (
<div className="user-detail-wrapper">
<img src={profile_img} />
<h4>{full_name}</h4>
<p>{title}</p>
</div>
)
}
export default React.memo(UserDetails)
此方法將基于嚴格相等對組件的 props
和上下文進行淺層比較驾锰。
15.使用 CSS 動畫代替 JS 動畫
動畫可以提供更加流暢優(yōu)秀的用戶體驗。實現(xiàn)動畫的方式有很多走越,一般來說椭豫,有三種方式可以創(chuàng)建動畫:
- CSS transitions
- CSS animations
- JavaScript
我們選擇哪一個取決于我們想要添加的動畫類型。
何時使用基于CSS的動畫:
- 添加 “一次性” 的轉(zhuǎn)換效果旨指,比如切換
UI
元素的狀態(tài)赏酥。 - 較小的自身包含狀態(tài)的 UI 元素。例如谆构,顯示氣泡提示裸扶,或者為菜單項增加懸停效果。
何時使用基于JavaScript的動畫:
- 當你想擁有高級效果時搬素,例如彈跳呵晨,停止,暫停蔗蹋,倒帶,減速或反轉(zhuǎn);
- 當你需要對動畫進行重度控制時;
- 當你需要切換動畫時囱淋,如鼠標懸停猪杭,點擊等;
- 當使用
requestAnimationFrame
來變化視覺時。
例如妥衣,假設(shè)您希望 div
在鼠標懸停時分為 4 個狀態(tài)設(shè)置動畫皂吮。div
在旋轉(zhuǎn) 90 度的過程中,四個階段將背景顏色從紅色變?yōu)樗{色税手,藍色變?yōu)榫G色蜂筹,綠色變?yōu)辄S色。在這種情況下芦倒,您需要結(jié)合使用JavaScript動畫和CSS轉(zhuǎn)換來更好地控制操作和狀態(tài)更改艺挪。
16.使用CDN
CDN是一種將網(wǎng)站或移動應用程序中的靜態(tài)內(nèi)容更快速有效地傳遞給用戶的絕佳方式。
CDN取決于用戶的地理位置兵扬。最靠近用戶的CDN服務(wù)器稱為“邊緣服務(wù)器”麻裳。當用戶從您的網(wǎng)站請求通過CDN提供的內(nèi)容時,他們會連接到邊緣服務(wù)器并確保最佳的在線體驗器钟。
有一些很棒的CDN提供商津坑。例如,CloudFront傲霸,CloudFlare疆瑰,Akamai眉反,MaxCDN,Google Cloud CDN等穆役。
您也可以選擇Netlify或Surge.sh在CDN上托管您的靜態(tài)內(nèi)容寸五。Surge是一款免費的CLI工具,可將您的靜態(tài)項目部署到生產(chǎn)質(zhì)量的CDN孵睬。
17.在CPU擴展任務(wù)中使用 Web Workers
Web Workers
可以在Web應用程序的后臺線程中運行腳本操作播歼,與主執(zhí)行線程分開。通過在單獨的線程中執(zhí)行費力的處理掰读,主線程(通常是UI)能夠在不被阻塞或減速的情況下運行秘狞。
在相同的執(zhí)行上下文中,由于JavaScript是單線程的蹈集,我們需要并行計算烁试。這可以通過兩種方式實現(xiàn)。第一個選項是使用偽并行拢肆,它基于 setTimeout
函數(shù)實現(xiàn)的减响。第二種選擇是使用 Web Workers
。
Web Workers
在執(zhí)行計算擴展操作時效果最佳郭怪,因為它在后臺的單獨線程中獨立于其他腳本執(zhí)行代碼支示。這意味著它不會影響頁面的性能。
我們可以利用React
中的Web Workers
來執(zhí)行計算昂貴的任務(wù)鄙才。
這是不使用 Web Workers
的代碼:
// Sort Service for sort post by the number of comments
function sort(posts) {
for (let index = 0, len = posts.length - 1; index < len; index++) {
for (let count = index+1; count < posts.length; count++) {
if (posts[index].commentCount > posts[count].commentCount) {
const temp = posts[index];
posts[index] = users[count];
posts[count] = temp;
}
}
}
return posts;
}
export default Posts extends React.Component{
constructor(props){
super(posts);
}
state = {
posts: this.props.posts
}
doSortingByComment = () => {
if(this.state.posts && this.state.posts.length){
const sortedPosts = sort(this.state.posts);
this.setState({
posts: sortedPosts
});
}
}
render(){
const posts = this.state.posts;
return (
<React.Fragment>
<Button onClick={this.doSortingByComment}>
Sort By Comments
</Button>
<PostList posts={posts}></PostList>
</React.Fragment>
)
}
}
當我們有 20,000
個帖子時會發(fā)生什么颂鸿?由于 sort
方法時間復雜度 O(n^2)
,它將減慢渲染速度攒庵,因為它們在同一個線程中運行嘴纺。
下面是修改后的代碼,它使用 Web Workers
來處理排序:
// sort.worker.js
// In-Place Sort function for sort post by number of comments
export default function sort() {
self.addEventListener('message', e =>{
if (!e) return;
let posts = e.data;
for (let index = 0, len = posts.length - 1; index < len; index++) {
for (let count = index+1; count < posts.length; count++) {
if (posts[index].commentCount > posts[count].commentCount) {
const temp = posts[index];
posts[index] = users[count];
posts[count] = temp;
}
}
}
postMessage(posts);
});
}
export default Posts extends React.Component{
constructor(props){
super(posts);
}
state = {
posts: this.props.posts
}
componentDidMount() {
this.worker = new Worker('sort.worker.js');
this.worker.addEventListener('message', event => {
const sortedPosts = event.data;
this.setState({
posts: sortedPosts
})
});
}
doSortingByComment = () => {
if(this.state.posts && this.state.posts.length){
this.worker.postMessage(this.state.posts);
}
}
render(){
const posts = this.state.posts;
return (
<React.Fragment>
<Button onClick={this.doSortingByComment}>
Sort By Comments
</Button>
<PostList posts={posts}></PostList>
</React.Fragment>
)
}
}
在這段代碼中浓冒,我們sort在單獨的線程中運行該方法栽渴,這將確保我們不會阻塞主線程。
您可以考慮使用 Web Workers
執(zhí)行圖像處理稳懒,排序闲擦,過濾和其他消耗高昂 CPU 性能的任務(wù)。
參考: 使用Web Workers
18.虛擬化長列表
虛擬化列表或窗口化是一種在呈現(xiàn)長數(shù)據(jù)列表時提高性能的技術(shù)场梆。此技術(shù)在任何時間內(nèi)只展現(xiàn)列表的一部分佛致,并且可以顯著減少重新渲染組件所花費的時間,以及創(chuàng)建 DOM 節(jié)點的總數(shù)辙谜。
有一些流行的 React
庫俺榆,如react-window
和react-virtualized
,它提供了幾個可重用的組件來展示列表装哆,網(wǎng)格和表格數(shù)據(jù)罐脊。
19.分析和優(yōu)化您的 Webpack
打包
在生產(chǎn)部署之前定嗓,您應該檢查并分析應用程序包以刪除不需要的插件或模塊。
您可以考慮使用Webpack Bundle Analyzer
萍桌,它允許您使用可視化的樹狀結(jié)構(gòu)來查看 webpack
輸出文件的大小宵溅。
該模塊將幫助您:
- 了解你的打包內(nèi)容
- 找出最大尺寸的模塊
- 找到哪些模塊有錯誤
- 優(yōu)化它!
最好的優(yōu)點是什么上炎?它支持壓縮模塊恃逻!他在解析他們以獲得模塊的真實大小,同時展示壓縮大信菏寇损!下面是使用 webpack-bundle-analyzer
的示例:
[圖片上傳失敗...(image-73b04-1563263103103)]
20.考慮服務(wù)端渲染
服務(wù)端渲染的好處之一是為用戶提供更好的體驗,相比客戶端渲染裳食,用戶會更快接受到可查看的內(nèi)容矛市。
近年來,像沃爾瑪和Airbnb會使用 React
服務(wù)端渲染來為用戶提供更好的用戶體驗诲祸。然而浊吏,在服務(wù)器上呈現(xiàn)擁有大數(shù)據(jù),密集型應用程序很快就會成為性能瓶頸救氯。
服務(wù)器端渲染提供了性能優(yōu)勢和一致的SEO表現(xiàn)≌姨铮現(xiàn)在,如果您在沒有服務(wù)器端渲染的情況下檢查React應用程序頁面源着憨,它將如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="/favicon.ico">
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script src="/app.js"></script>
</body>
</html>
瀏覽器還將獲取app.js
包含應用程序代碼的包墩衙,并在一兩秒后呈現(xiàn)整個頁面。
[圖片上傳失敗...(image-1da43c-1563263103103)]
我們可以看到客戶端渲染享扔,在到達服務(wù)器之前有兩次往返底桂,用戶可以看到內(nèi)容≈才郏現(xiàn)在惧眠,如果應用程序包含API驅(qū)動的數(shù)據(jù)呈現(xiàn),那么流程中會有一個暫停于个。
讓我們考慮用服務(wù)器端渲染來處理的同一個應用程序:
[圖片上傳失敗...(image-6a1144-1563263103102)]
我們看到在用戶獲取內(nèi)容之前氛魁,只有一次訪問服務(wù)器。那么服務(wù)器究竟發(fā)生了什么厅篓?當瀏覽器請求頁面時秀存,服務(wù)器會在內(nèi)存中加載React
并獲取呈現(xiàn)應用程序所需的數(shù)據(jù)。之后羽氮,服務(wù)器將生成的HTML
發(fā)送到瀏覽器或链,立即向用戶顯示內(nèi)容。
以下是一些為React應用程序提供SSR的流行解決方案:
- Next.js
- Gatsby
21.在Web服務(wù)器上啟用Gzip壓縮
Gzip
壓縮允許 Web
服務(wù)器提供更小的文件大小档押,這意味著您的網(wǎng)站加載速度更快澳盐。gzip
運行良好的原因是因為JavaScript
祈纯,CSS
和HTML
文件使用了大量具有大量空白的重復文本。由于gzip
壓縮常見字符串叼耙,因此可以將頁面和樣式表的大小減少多達70%
腕窥,從而縮短網(wǎng)站的首次渲染時間。
如果您使用的是 Node / Express
后端筛婉,則可以使用 Gzipping
來解決這個問題簇爆。
const express = require('express');
const compression = require('compression');
const app = express();
// Pass `compression` as a middleware!
app.use(compression());
結(jié)論
有許多方法可以優(yōu)化React
應用程序,例如延遲加載組件爽撒,使用 ServiceWorkers
緩存應用程序狀態(tài)入蛆,考慮SSR
,避免不必要的渲染等等匆浙。也就是說安寺,在考慮優(yōu)化之前,值得了解React
組件如何工作首尼,理解 diff
算法挑庶,以及在React
中 render
的工作原理。這些都是優(yōu)化應用程序時需要考慮的重要概念软能。
我認為沒有測量的優(yōu)化幾乎都是為時過早的迎捺,這就是為什么我建議首先對性能進行基準測試和測量。您可以考慮使用 Chrome
時間線分析和可視化組件查排。這使您可以查看卸載凳枝,裝載,更新哪些組件以及它們相對于彼此的時間跋核。它將幫助您開始性能優(yōu)化之旅岖瑰。