原文: 21 Performance Optimization Techniques for React Apps
做者:Nishant
譯者:博軒
在 React
內部,React
會使用幾項巧妙的小技術,來優化計算更新 UI
時,所須要的最少的更新 DOM
的操做。在大多數狀況下,即便你沒有針對性能進行專項優化,React
依然很快,可是仍有一些方法能夠加速 React
應用程序。本文將介紹一些可用於改進 React
代碼的有效技巧。javascript
數據不變性不是一種架構或者設計模式,它是一種編程思想。它會強制您考慮如何構建應用程序的數據流。在我看來,數據不變性是一種符合嚴格單項數據流的實踐。php
數據不變性,這一來自函數式編程的概念,可應用於前端應用程序的設計。它會帶來不少好處,例如:html
在 React
環境中,咱們使用 Component
的概念來維護組件內部的狀態,對狀態的更改能夠致使組建的從新渲染。前端
React
構建並在內部維護呈現的UI(Virtual DOM)。當組件的 props
或者 state
發生改變時,React
會將新返回的元素與先前呈現的元素進行比較。當二者不相等時,React
將更新 DOM。所以,在改變狀態時,咱們必需要當心。java
讓咱們考慮一個用戶列表組件:react
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});
}複製代碼
這裏的關注點是,咱們正在將新的用戶添加到變量 users
,這裏它對應的引用是 this.state.users
。webpack
專業提示 : 應該將 React
的狀態視爲不可變。咱們不該該直接修改 this.state
,由於 setState()
以後的調用可能會覆蓋你以前的修改。git
那麼,若是咱們直接修改 state
會產生什麼問題呢?比方說,咱們添加 shouldComponentUpdate
,並對比 nextState
和 this.state
來確保只有當數據改變時,纔會從新渲染組件。es6
shouldComponentUpdate(nextProps, nextState) {
if (this.state.users !== nextState.users) {
return true;
}
return false;
}複製代碼
即便用戶的數組發生了改變,React
也不會從新渲染UI了,由於他們的引用是相同的。github
避免此類問題最簡單的方法,就是避免直接修改 props
和 state
。因此,咱們可使用 concat
來重寫 addNewUser
方法:
addNewUser = () => {
this.setState(state => ({
users: state.users.concat({
timeStamp: new Date(),
userName: "robin",
email: "email@email.com"
})
}));
};複製代碼
爲了處理 React
組件中 props
或者 state
的改變,咱們能夠考慮一下幾種處理不可變的方法:
[].concat
或es6的 [ ...params]
Object.assign({}, ...)
或 es6的 {...params}
在向代碼庫引入不變性時,這兩種方法有很長的路要走。
可是,最好使用一個提供不可變數據結構的優化庫。如下是您可使用的一些庫:
JavaScript
數據結構的庫,他與普通的數組和對象向後兼容。專業提示: React setState
方法是異步的。這意味着,setState()
方法會建立一個帶轉換的 state
, 而不是當即修改 this.state
。若是在調用setState()
方法以後去訪問 this.state
,則可能會返回現有值。爲防止這種狀況,請setState
在調用完成後使用回調函數運行代碼。
其餘資源:
在 React
中,函數組件和 PureComponent
提供了兩種不一樣級別的組件優化方案。
函數組件防止了構造類實例,
同時函數組件能夠減小總體包的大小,由於它比類組件的的體積更小。
另外一方面,爲了優化UI更新,咱們能夠考慮將函數組件轉換爲 PureComponent
類組件(或使用自定義 shouldComponentUpdate
方法的類)。可是,若是組件不使用狀態和其餘生命週期方法,爲了達到更快的的更新,首次渲染相比函數組件會更加複雜一些。
譯註:函數組件也能夠作純組件的優化:React.memo(...) 是 React v16.6 中引入的新功能。 它與 React.PureComponent 相似,它有助於控制函數組件的從新渲染,以及memoized。 React.memo(...) 對應的是函數組件,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
對狀態變化進行淺層比較(shallow comparison)。這意味着它在比較時,會比較原始數據類型的值,並比較對象的引用。所以,咱們必須確保在使用 React.PureComponent
時符合兩個標準:
State
/ Props
是一個不可變對象;State
/ Props
不該該有多級嵌套對象。專業提示: 全部使用 React.PureComponent
的子組件,也應該是純組件或函數組件。
Multiple Chunk Files
您的應用程序始終以一些組件開始。您開始添加新功能和依賴項,最終您會獲得一個巨大的生產文件。
您能夠考慮經過利用 CommonsChunkPlugin for webpack 將供應商或第三方庫代碼與應用程序代碼分開,生成兩個單獨的文件。你最終會獲得 vendor.bundle.js
和 app.bundle.js
。經過拆分文件,您的瀏覽器會緩存較少的資源,同時並行下載資源以減小等待的加載時間。
注意: 若是您使用的是最新版本的webpack,還能夠考慮使用 SplitChunksPlugin
若是您使用 webpack 4
做爲應用程序的模塊捆綁程序,則能夠考慮將 mode
選項設置爲 production
。這基本上告訴 webpack
使用內置優化:
module.exports = {
mode: 'production'
};複製代碼
或者,您能夠將其做爲 CLI
參數傳遞:
webpack --mode=production複製代碼
這樣作會限制對庫的優化,例如縮小或刪除開發代碼。它不會公開源代碼,文件路徑等等。
在考慮優化程序包大小的時候,檢查您的依賴項中實際有多少代碼被使用了,會頗有價值。例如,若是您使用 Moment.js
會包含本地化文件的多語言支持。若是您不須要支持多種語言,那麼您能夠考慮使用 moment-locales-webpack-plugin 來刪除不須要的語言環境。
另外一個例子是使用 lodash
。假設你使用了 100 多種方法的 20 種,那麼你最終打包時其餘額外的方法都是不須要的。所以,可使用 lodash-webpack-plugin 來刪除未使用的函數。
如下是一些使用 Webpack
打包時可選的依賴項優化列表
React.fragments
容許您在不添加額外節點的狀況下對子列表進行分組。
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>
</>
);
}
}複製代碼
因爲在 JavaScript
中函數就是對象({} !== {}),所以當 React
進行差別檢查時,內聯函數將始終使 prop diff
失敗。此外,若是在JSX屬性中使用箭頭函數,它將在每次渲染時建立新的函數實例。這可能會爲垃圾收集器帶來不少工做。
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}/>
})
)
}
}複製代碼
您能夠定義箭頭函數,而不是爲 props
定義內聯函數。
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}/>
})
)
}
}複製代碼
事件觸發率表明事件處理程序在給定時間內調用的次數。
一般,與滾動和鼠標懸停相比,鼠標點擊具備較低的事件觸發率。較高的事件觸發率有時會使應用程序崩潰,但能夠對其進行控制。
咱們來討論一些技巧。
首先,明確事件處理會帶來一些昂貴的操做。例如,執行UI更新,處理大量數據或執行計算昂貴任務的XHR請求或DOM操做。在這些狀況下,防抖和節流技術能夠成爲救世主,而不會對事件監聽器進行任何更改。
簡而言之,節流意味着延遲功能執行。所以,不是當即執行事件處理程序/函數,而是在觸發事件時添加幾毫秒的延遲。例如,這能夠在實現無限滾動時使用。您能夠延遲 XHR
調用,而不是在用戶滾動時獲取下一個結果集。
另外一個很好的例子是基於 Ajax
的即時搜索。您可能不但願每次按鍵時,都會請求服務器獲取新的數據,所以最好節流直到輸入字段處於休眠狀態幾毫秒以後,再請求數據。
節流能夠經過多種方式實現。您能夠限制觸發的事件的次數或延遲正在執行的事件來限制程序執行一些昂貴的操做。
與節流不一樣,防抖是一種防止事件觸發器過於頻繁觸發的技術。若是您正在使用 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
,可使用簡單版的防抖函數。
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}}複製代碼
在渲染列表時,您常常會看到索引被用做鍵。
{
comments.map((comment, index) => {
<Comment
{..comment}
key={index} />
})
}複製代碼
可是使用 index
做爲 key
, 被用在React虛擬DOM元素
的時候,會使你的應用可能出現錯誤的數據 。當您從列表中添加或刪除元素時,若是該 key
與之前相同,則 React虛擬DOM元素
表示相同的組件。
始終建議使用惟一屬性做爲 key
,或者若是您的數據沒有任何惟一屬性,那麼您能夠考慮使用shortid module
生成惟一 key
的屬性。
import shortid from "shortid";
{
comments.map((comment, index) => {
<Comment
{..comment}
key={shortid.generate()} />
})
}複製代碼
可是,若是數據具備惟一屬性(例如ID),則最好使用該屬性。
{
comments.map((comment, index) => {
<Comment
{..comment}
key={comment.id} />
})
}複製代碼
在某些狀況下,將 index
用做 key
是徹底能夠的,但僅限於如下條件成立時:
咱們常常須要將帶有 props
的初始數據傳遞給 React組件
來設置初始狀態值。
考慮一下這段代碼:
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
變化會發生什麼?它會映射到 state
中嘛? 若是在沒有刷新組件的狀況下,props
中的值被修改了,props
中的值,將永遠不會分配給 state
中的 applyCoupon
。這是由於構造函數僅在EditPanel
組件首次建立時被調用。
引用React文檔:
避免將 props 的值複製給 state!這是一個常見的錯誤:
constructor(props) {
super(props);
// 不要這樣作
this.state = { color: props.color };
}複製代碼
如此作毫無必要(你能夠直接使用 this.props.color),同時還產生了 bug(更新 prop 中的 color 時,並不會影響 state)。**只有在你刻意忽略props
更新的狀況下使用。此時,應將props
重命名爲initialColor
或defaultColor
。必要時,你能夠修改它的key
,以強制「重置」其內部state
。請參閱博文: 你可能不須要派生 state
props
中的屬性class EditPanelComponent extends Component {
render(){
return <div>{this.props.applyCoupon &&
<>Enter Coupon:<Input/></>}</div>
}
}複製代碼
props
改變時,您可使用 componentDidUpdate
函數來更新 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>
}
}複製代碼
您應該避免將屬性傳播到 DOM
元素中,由於它會添加未知的 HTML
屬性,這是沒必要要的,也是一種很差的作法。
const CommentsText = props => {
return (
<div {...props}> {props.text} </div>
);
};複製代碼
您能夠設置特定屬性,而不是直接傳遞 Props:
const CommentsText = props => {
return (
<div specificAttr={props.specificAttr}> {props.text} </div>
);
};複製代碼
Reselect
是 Redux
的一個簡單的選擇器庫,可用於構建記憶選擇器。您能夠將選擇器定義爲函數,爲 React
組件檢索 Redux
狀態片斷。
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);複製代碼
在這段代碼中,每次評論數據在 state
中 變化時,CommentsContainer
和 ShareContainer
將會從新渲染。即便 addStaticPath
不進行任何數據更改也會發生這種狀況,由於 socialDetails
由 addStaticPath
函數返回的的數據每次都是一個新的對象 (請記住{} != {})。如今,若是咱們用 Reselect
重寫 addStaticPath
,問題就會消失,由於Reselect
將返回最後一個函數結果,直到它傳遞新的輸入。
import { createSelector } from "reselect";
const socialPathSelector = state => state.social;
const addStaticPath = createSelector(
socialPathSelector,
social => ({
iconPath: `../../image/${social.iconPath}`
})
);複製代碼
Memoization是一種用於優化程序速度的技術,主要經過存儲複雜函數的計算結果,當再次出現相同的輸入,直接從緩存中返回結果。memoized
函數一般更快,由於若是使用與前一個函數相同的值調用函數,則不會執行函數邏輯,而是從緩存中獲取結果。
讓咱們考慮下面簡單的無狀態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
發生變化,這個無狀態組件就會從新渲染。若是 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
和上下文進行淺層比較。
動畫能夠提供更加流暢優秀的用戶體驗。實現動畫的方式有不少,通常來講,有三種方式能夠建立動畫:
咱們選擇哪個取決於咱們想要添加的動畫類型。
UI
元素的狀態。requestAnimationFrame
來變化視覺時。例如,假設您但願 div
在鼠標懸停時分爲 4 個狀態設置動畫。div
在旋轉 90 度的過程當中,四個階段將背景顏色從紅色變爲藍色,藍色變爲綠色,綠色變爲黃色。在這種狀況下,您須要結合使用JavaScript動畫和CSS轉換來更好地控制操做和狀態更改。
CDN是一種將網站或移動應用程序中的靜態內容更快速有效地傳遞給用戶的絕佳方式。
CDN取決於用戶的地理位置。最靠近用戶的CDN服務器稱爲「邊緣服務器」。當用戶從您的網站請求經過CDN提供的內容時,他們會鏈接到邊緣服務器並確保最佳的在線體驗。
有一些很棒的CDN提供商。例如,CloudFront,CloudFlare,Akamai,MaxCDN,Google Cloud CDN等。
您也能夠選擇Netlify或Surge.sh在CDN上託管您的靜態內容。Surge是一款免費的CLI工具,可將您的靜態項目部署到生產質量的CDN。
Web Workers
能夠在Web應用程序的後臺線程中運行腳本操做,與主執行線程分開。經過在單獨的線程中執行費力的處理,主線程(一般是UI)可以在不被阻塞或減速的狀況下運行。
在相同的執行上下文中,因爲JavaScript是單線程的,咱們須要並行計算。這能夠經過兩種方式實現。第一個選項是使用僞並行,它基於 setTimeout
函數實現的。第二種選擇是使用 Web Workers
。
Web Workers
在執行計算擴展操做時效果最佳,由於它在後臺的單獨線程中獨立於其餘腳本執行代碼。這意味着它不會影響頁面的性能。
咱們能夠利用React
中的Web Workers
來執行計算昂貴的任務。
這是不使用 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
個帖子時會發生什麼?因爲 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
執行圖像處理,排序,過濾和其餘消耗高昂 CPU 性能的任務。
參考: 使用Web Workers
虛擬化列表或窗口化是一種在呈現長數據列表時提升性能的技術。此技術在任什麼時候間內只展示列表的一部分,而且能夠顯著減小從新渲染組件所花費的時間,以及建立 DOM 節點的總數。
有一些流行的 React
庫,如react-window
和react-virtualized
,它提供了幾個可重用的組件來展現列表,網格和表格數據。
在生產部署以前,您應該檢查並分析應用程序包以刪除不須要的插件或模塊。
您能夠考慮使用Webpack Bundle Analyzer
,它容許您使用可視化的樹狀結構來查看 webpack
輸出文件的大小。
該模塊將幫助您:
最好的優勢是什麼?它支持壓縮模塊!他在解析他們以得到模塊的真實大小,同時展現壓縮大小!下面是使用 webpack-bundle-analyzer
的示例:
服務端渲染的好處之一是爲用戶提供更好的體驗,相比客戶端渲染,用戶會更快接受到可查看的內容。
近年來,像沃爾瑪和Airbnb會使用 React
服務端渲染來爲用戶提供更好的用戶體驗。然而,在服務器上呈現擁有大數據,密集型應用程序很快就會成爲性能瓶頸。
服務器端渲染提供了性能優點和一致的SEO表現。如今,若是您在沒有服務器端渲染的狀況下檢查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
包含應用程序代碼的包,並在一兩秒後呈現整個頁面。
咱們能夠看到客戶端渲染,在到達服務器以前有兩次往返,用戶能夠看到內容。如今,若是應用程序包含API驅動的數據呈現,那麼流程中會有一個暫停。
讓咱們考慮用服務器端渲染來處理的同一個應用程序:
咱們看到在用戶獲取內容以前,只有一次訪問服務器。那麼服務器究竟發生了什麼?當瀏覽器請求頁面時,服務器會在內存中加載React
並獲取呈現應用程序所需的數據。以後,服務器將生成的HTML
發送到瀏覽器,當即向用戶顯示內容。
如下是一些爲React應用程序提供SSR的流行解決方案:
Gzip
壓縮容許 Web
服務器提供更小的文件大小,這意味着您的網站加載速度更快。gzip
運行良好的緣由是由於JavaScript
,CSS
和HTML
文件使用了大量具備大量空白的重複文本。因爲gzip
壓縮常見字符串,所以能夠將頁面和樣式表的大小減小多達70%
,從而縮短網站的首次渲染時間。
若是您使用的是 Node / Express
後端,則可使用 Gzipping
來解決這個問題。
const express = require('express');
const compression = require('compression');
const app = express();
// Pass `compression` as a middleware!
app.use(compression());複製代碼
有許多方法能夠優化React
應用程序,例如延遲加載組件,使用 ServiceWorkers
緩存應用程序狀態,考慮SSR
,避免沒必要要的渲染等等。也就是說,在考慮優化以前,值得了解React
組件如何工做,理解 diff
算法,以及在React
中 render
的工做原理。這些都是優化應用程序時須要考慮的重要概念。
我認爲沒有測量的優化幾乎都是爲時過早的,這就是爲何我建議首先對性能進行基準測試和測量。您能夠考慮使用 Chrome
時間線分析和可視化組件。這使您能夠查看卸載,裝載,更新哪些組件以及它們相對於彼此的時間。它將幫助您開始性能優化之旅。
若是您有任何其餘基於 React
的應用程序優化建議,歡迎評論區討論鴨。
本文已經聯繫原文做者,並受權翻譯,轉載請保留原文連接