環(huán)球網(wǎng)今日疫情消息網(wǎng)站優(yōu)化招聘
背景
雙十一大促時(shí),客戶客服那邊反饋商品信息加載卡頓,在不斷有訂單咨詢時(shí),甚至出現(xiàn)了商品信息一直處于加載狀態(tài)的情況,顯然,在這種高峰期接待客戶時(shí),是沒法進(jìn)行正常的接待工作的。
起初,頁面一直處于加載狀態(tài),初步認(rèn)為是后端接口返回太慢導(dǎo)致,后經(jīng)過后端日志排查,發(fā)現(xiàn)接口返回很快,根本不會(huì)造成頁面一直處于加載狀態(tài),甚至出現(xiàn)卡死的狀態(tài)。后經(jīng)過不斷排查,發(fā)現(xiàn)是客戶端性能問題導(dǎo)致。
優(yōu)化前
咨詢訂單時(shí),只咨詢一條訂單,用時(shí)需要3秒左右,當(dāng)連續(xù)咨詢5、6條訂單時(shí),用時(shí)甚至達(dá)到了一分多鐘,僅僅5、6條訂單竟然用時(shí)這么久,那么在持續(xù)不斷有訂單咨詢時(shí),頁面就會(huì)出現(xiàn)一直加載,甚至卡死的狀態(tài),明顯存在很大的性能問題。
利用performance工具可以分析主線程的Event Loop,圖中標(biāo)出的Main就是主線程。
主線程是不斷執(zhí)行 Event Loop 的,可以看到有很多個(gè) Task(宏任務(wù)),當(dāng)主線程中的任務(wù)過多時(shí),會(huì)導(dǎo)致主線程長(zhǎng)時(shí)間被占用,無法及時(shí)響應(yīng)用戶的交互操作,從而影響用戶體驗(yàn)。這種情況下,頁面可能會(huì)出現(xiàn)卡頓、延遲響應(yīng)等問題。
優(yōu)化后
當(dāng)只咨詢一條訂單時(shí),用時(shí)需要1秒時(shí)間,連續(xù)咨詢5、6條訂單,用時(shí)優(yōu)化到只需要3秒時(shí)間,并且頁面流暢,對(duì)于用戶體驗(yàn)上得到了明顯的提升。
可以看出long task 減少了很多。
那么,如何來優(yōu)化呢?請(qǐng)看下面的內(nèi)容。
優(yōu)化點(diǎn)
在合適的時(shí)機(jī)進(jìn)行組件渲染
在排查代碼的過程中發(fā)現(xiàn),很多本不該當(dāng)前狀態(tài)渲染的組件,都渲染出來了,顯然這是不合理的。過多的組件渲染會(huì)占用大量的內(nèi)存,并且也會(huì)增加頁面的渲染時(shí)間,自然,響應(yīng)性能就會(huì)變得很差,用戶與頁面的交互就會(huì)變得遲緩。
而商品信息加載部分最常見的不必要的組件渲染表現(xiàn)在使用Modal彈窗時(shí),我們都知道當(dāng)visible為true時(shí),會(huì)彈出彈窗相應(yīng)的頁面內(nèi)容,但是當(dāng)visible為false時(shí),其實(shí)是不希望渲染Modal彈窗中的內(nèi)容的,這會(huì)帶來額外的性能開銷。
下面是一些示例:
- ...
- <Modal
- ...
- visible={editVisible}
- ...>
- ...
- </Modal>
- ...
+ {editVisible && (
+ <GoodsAttributeModal
+ editVisible
+ ...
+ />
+ )}
// 把Modal彈窗作為一個(gè)單獨(dú)組件提取出去,并且只有當(dāng)editVisible為true時(shí)才渲染組件
第一段代碼中,使用了visible={editVisible}來控制Modal組件的顯示與隱藏。當(dāng)editVisible為true時(shí),Modal組件會(huì)被渲染出來,否則不會(huì)被渲染。
第二段代碼中,使用了條件渲染的方式,即通過{editVisible && …}來判斷是否渲染Modal組件。當(dāng)editVisible為true時(shí),Modal組件會(huì)被渲染出來,否則不會(huì)被渲染。
這兩種方式的主要區(qū)別在于組件的渲染時(shí)機(jī)。在第一種方式中,Modal組件在每次渲染時(shí)都會(huì)被創(chuàng)建和銷毀,而在第二種方式中,只有在editVisible為true時(shí)才會(huì)創(chuàng)建和渲染Modal組件。
使用條件渲染的方式可以提高性能,特別是在組件層級(jí)較深或渲染頻繁的情況下。因?yàn)橹挥性谛枰@示Modal組件時(shí)才會(huì)進(jìn)行渲染,避免了不必要的組件創(chuàng)建和銷毀,減少了內(nèi)存消耗和渲染時(shí)間。
總結(jié)起來,使用條件渲染的方式可以根據(jù)需要?jiǎng)討B(tài)地控制組件的顯示與隱藏,提高性能和用戶體驗(yàn)。
使用useCallback、useMemo、React.memo提升性能
下面是一些示例:
useCallback
- renderContent = (content, searchKey) => {
- if(content) {
- const contentWithBr = content.replace(/\?/g, '<br>').replace(/\n/g, '<br>')
- const regex = new RegExp(`(${searchKey})`, 'gi'); // 創(chuàng)建正則表達(dá)式,忽略大小寫匹配
- const matches = content.match(regex) || []; // 匹配結(jié)果數(shù)組
- return (
- <React.Fragment>
- {contentWithBr.split('<br>').map((text, index) => (
- <React.Fragment key={index}>
- {index > 0 && <br />}
- {text.split(regex).map((subText, subIndex) => {
- // console.log('subText',subText,matches)
- return (
- <React.Fragment key={subIndex}>
- {matches.includes(subText) ? (
- <span style={{ color: '#FF8800' }}>{subText}</span>
- ) : (
- subText
- )}
- </React.Fragment>
- )
- })}
- </React.Fragment>
- ))}
- </React.Fragment>
- )
- } else {
- return '-'
- }
- }+ const renderContent = useCallback((content, searchKey) => {
+ if (content) {
+ const contentWithBr = content.replace(/\?/g, '<br>').replace(/\n/g, '<br>')
+ const regex = new RegExp(`(${searchKey})`, 'gi') // 創(chuàng)建正則表達(dá)式,忽略大小寫匹配
+ const matches = content.match(regex) || [] // 匹配結(jié)果數(shù)組
+ return (
+ <React.Fragment>
+ {contentWithBr.split('<br>').map((text, index) => (
+ <React.Fragment key={index}>
+ {index > 0 && <br />}
+ {text.split(regex).map((subText, subIndex) => {
+ //console.log('subText',subText,matches)
+ return (
+ <React.Fragment key={subIndex}>
+ {matches.includes(subText) ? (
+ <span style={{ color: '#FF8800' }}>{subText}</span>
+ ) : (
+ subText
+ )}
+ </React.Fragment>
+ )
+ })}
+ </React.Fragment>
+ ))}
+ </React.Fragment>
+ )
+ } else {
+ return '-'
+ }
+ }, [])
上面的代碼使用了React的useCallback鉤子函數(shù)來定義了一個(gè)名為renderContent的函數(shù)。useCallback的作用是用來緩存函數(shù),以便在依賴項(xiàng)不變的情況下避免函數(shù)的重新創(chuàng)建。
使用useCallback的好處是可以優(yōu)化性能,特別是在父組件重新渲染時(shí),避免不必要的函數(shù)重新創(chuàng)建。當(dāng)依賴項(xiàng)數(shù)組為空時(shí),useCallback會(huì)在組件的初始渲染時(shí)創(chuàng)建函數(shù),并在后續(xù)的渲染中重復(fù)使用同一個(gè)函數(shù)。
而沒有使用useCallback的情況下,每次組件重新渲染時(shí)都會(huì)創(chuàng)建一個(gè)新的renderContent函數(shù),即使函數(shù)的實(shí)現(xiàn)邏輯完全相同。這可能會(huì)導(dǎo)致性能問題,特別是在組件層級(jí)較深或渲染頻繁的情況下。
因此,使用useCallback可以提高組件的性能,避免不必要的函數(shù)創(chuàng)建和內(nèi)存消耗。但需要注意的是,只有在確實(shí)需要緩存函數(shù)并且依賴項(xiàng)不變的情況下才使用useCallback,否則可能會(huì)導(dǎo)致不必要的優(yōu)化和錯(cuò)誤。
useMemo
- const tooltip = (
- <div>
- <h2>
- <span className={styles.title}>{title}</span>
- {
- !window.isVisibleGoods && (
- <span>
- {renderKnowledgeModal({
- label: '編輯',
- record: item,
- platGoodsId: plat_goods_id,
- classification_id: classificationId,
- })}
- <a
- className={styles.delete}
- onClick={() => handleDeleteKnowledage(item, classificationId)}
- >
- 刪除
- </a>
- </span>
- )
- }
- </h2>
- <div className={styles.img_block}>{images}</div>
- <div
- className={classnames(styles.context, styles.tooltipsContext)}
- dangerouslySetInnerHTML={{ __html: ParseBrow.parse(context) }}
- />
- </div>
- )
+ const tooltip = useMemo(
+ () => (
+ <div>
+ <h2>
+ <span className={styles.title}>{title}</span>
+ {!isVisibleGoods && (
+ <span>
+ {renderKnowledgeModal({
+ label: '編輯',
+ record: item,
+ platGoodsId: plat_goods_id,
+ classification_id: classificationId,
+ })}
+ <a
+ className={styles.delete}
+ onClick={() => handleDeleteKnowledage(item, classificationId)}
+ >
+ 刪除
+ </a>
+ </span>
+ )}
+ </h2>
+ <div className={styles.img_block}>{images}</div>
+ <div
+ className={classnames(styles.context, styles.tooltipsContext)}
+ dangerouslySetInnerHTML={{ __html: ParseBrow.parse(context) }}
+ />
+ </div>
+ ),
+ [
+ title,
+ renderKnowledgeModal,
+ item,
+ plat_goods_id,
+ classificationId,
+ images,
+ context,
+ handleDeleteKnowledage,
+ isVisibleGoods,
+ ]
+ )
在上面的代碼中,使用了useMemo來緩存了一個(gè)變量tooltip的計(jì)算結(jié)果。這個(gè)計(jì)算結(jié)果是一個(gè)React元素,包含了一些子元素和事件處理函數(shù)等。通過將tooltip作為依賴數(shù)組的一部分,當(dāng)依賴數(shù)組中的值發(fā)生變化時(shí),useMemo會(huì)重新計(jì)算tooltip的值;如果依賴數(shù)組中的值沒有發(fā)生變化,則直接返回上一次緩存的tooltip的值。
這樣做的好處是,當(dāng)依賴數(shù)組中的值沒有發(fā)生變化時(shí),可以避免重復(fù)計(jì)算tooltip的值,提高組件的性能。而如果依賴數(shù)組中的值發(fā)生變化,useMemo會(huì)重新計(jì)算tooltip的值,確保tooltip的值是最新的。
相比之下,如果不使用useMemo,每次組件重新渲染時(shí)都會(huì)重新計(jì)算tooltip的值,即使依賴數(shù)組中的值沒有發(fā)生變化,這樣會(huì)造成不必要的性能損耗。
總結(jié)起來,使用useMemo可以優(yōu)化組件的性能,避免不必要的計(jì)算。但是需要注意的是,只有在計(jì)算的成本比較高時(shí)才需要使用useMemo,否則可能會(huì)帶來額外的開銷
React.memo
- export default Item
+ import { isEqual } from 'lodash'
+ export default React.memo(Item, isEqual)
export default Item 直接導(dǎo)出組件,每次父組件重新渲染都會(huì)重新渲染 Item 組件;
而 export default React.memo(Item, isEqual) 使用 React.memo 進(jìn)行包裹,并傳入自定義的比較函數(shù) isEqual,只有在 props 發(fā)生變化且通過 isEqual 函數(shù)比較不相等時(shí)才會(huì)重新渲染 Item 組件。
注意:自定義的比較函數(shù) isEqual 用于比較兩個(gè) props 是否相等。如果不傳入比較函數(shù),則默認(rèn)使用淺比較(即 Object.is)來比較 props。如果傳入了比較函數(shù),則會(huì)使用該函數(shù)來比較 props。
props解構(gòu)變量時(shí)的默認(rèn)值
在這段代碼中,KnowledgeTab是一個(gè)使用了React.memo進(jìn)行優(yōu)化的組件。React.memo是一個(gè)高階組件,用于對(duì)組件進(jìn)行淺層比較,以確定是否需要重新渲染組件。當(dāng)組件的props沒有發(fā)生變化時(shí),React.memo會(huì)返回之前渲染的結(jié)果,從而避免不必要的重新渲染。
在KnowledgeTab組件中,knowledge_list是一個(gè)從props中解構(gòu)出來的屬性。而const knowledge_list_default = useMemo(() => [], [])是使用useMemo鉤子函數(shù)創(chuàng)建的一個(gè)空數(shù)組。這樣做的目的是為了在組件的初始渲染時(shí),給knowledge_list一個(gè)默認(rèn)值,以避免在解構(gòu)時(shí)出現(xiàn)undefined的情況。
如果直接使用knowledge_list=[]來給knowledge_list賦值,會(huì)破壞React.memo的優(yōu)化。因?yàn)槊看胃附M件重新渲染時(shí),knowledge_list都會(huì)被重新創(chuàng)建,即使它的值沒有發(fā)生變化。這樣會(huì)導(dǎo)致KnowledgeTab組件的props發(fā)生變化,從而觸發(fā)不必要的重新渲染。
而使用useMemo創(chuàng)建一個(gè)空數(shù)組作為默認(rèn)值,可以保證在父組件重新渲染時(shí),knowledge_list_default的引用不會(huì)發(fā)生變化,從而避免不必要的重新渲染。這樣就能夠保持React.memo的優(yōu)化效果,只有在knowledge_list的值真正發(fā)生變化時(shí)才會(huì)重新渲染KnowledgeTab組件。
所以,總結(jié)起來就是默認(rèn)值如果傳給子組件,父組件每一次更新都會(huì)導(dǎo)致子組件更新,導(dǎo)致子組件的React.memo失效
拆分為狀態(tài)自治的獨(dú)立組件
當(dāng)一個(gè)組件的代碼變得復(fù)雜或包含大量的子組件時(shí),可以考慮將其中的一部分代碼抽取為一個(gè)獨(dú)立的子組件。這樣做的好處是可以將復(fù)雜的邏輯拆分為多個(gè)小組件,提高代碼的可讀性和可維護(hù)性。
同時(shí),抽取組件也可以配合使用React.memo進(jìn)行優(yōu)化。
下面是一個(gè)抽取獨(dú)立組件的例子
import React, { memo } from 'react'
import { Tooltip } from 'antd'
import classNames from 'classnames'
import Item from './item'
import styles from '../../index.less'interface Item {name: stringid: string
}
interface CategoryProps {item: ItemactiveKey: stringonClickItem: () => void
}
const Category: React.FC<CategoryProps> = props => {const { item, activeKey, onClickItem } = propsconst { name, id } = itemreturn (<Tooltiptitle={name}placement="topRight"align={{offset: [0, 5],}}><spankey={id}className={classNames(styles.tab_item, {[styles.active_item]: activeKey === id,})}onClick={onClickItem}>{name}</span></Tooltip>)
}export default memo(Category)