性能優化一直是前端避不開的話題,本文將會從如何加快首屏渲染和如何優化運行時性能兩方面入手,談一談我的在項目中的性能優化手段(不說 CSS 放頭部,減小 HTTP 請求等方式)javascript
一說到懶加載,可能更多人想到的是圖片懶加載,但懶加載能夠作的更多html
咱們在項目中常常會用到第三方的 JS 文件,好比 網易雲盾、明文加密庫、第三方的客服系統(zendesk)等,在接入這些第三方庫時,他們的接入文檔經常會告訴你,放在 head 中間,可是其實這些可能就是影響你首屏性能的兇手之一,咱們只須要用到它時,在把他引入便可前端
編寫一個簡單的加載腳本代碼:java
/** * 動態加載腳本 * @param url 腳本地址 */
export function loadScript(url: string, attrs?: object) {
return new Promise((resolve, reject) => {
const matched = Array.prototype.find.call(document.scripts, (script: HTMLScriptElement) => {
return script.src === url
})
if (matched) {
// 若是已經加載過,直接返回
return resolve()
}
const script = document.createElement('script')
if (attrs) {
Object.keys(attrs).forEach(name => {
script.setAttribute(name, attrs[name])
})
}
script.type = 'text/javascript'
script.src = url
script.onload = resolve
script.onerror = reject
document.body.appendChild(script)
})
}
複製代碼
有了加載腳本的代碼後,咱們配合加密密碼登陸使用react
// 明文加密的方法
async function encrypt(value: string): Promise<string> {
// 引入加密的第三方庫
await loadScript('/lib/encrypt.js')
// 配合 JSEncrypt 加密
const encrypt = new JSEncrypt()
encrypt.setPublicKey(PUBLIC_KEY)
const encrypted = encrypt.encrypt(value)
return encrypted
}
// 登陸操做
async function login() {
// 密碼加密
const password = await encrypt('12345')
await fetch('https://api/login', {
method: 'POST',
body: JSON.stringify({
password,
})
})
}
複製代碼
這樣子就能夠避免在用到以前引入 JSEncrypt,其他的第三方庫相似webpack
在如今的前端開發中,咱們可能比較少會運用 script 標籤引入第三方庫,更多的仍是選擇 npm install
的方式來安裝第三方庫,這個 loadScript 就無論用了git
咱們用 import()
的方式改寫一下上面的 encrypt
代碼github
async function encrypt(value: string): Promise<string> {
// 改成 import() 的方式引入加密的第三方庫
const module = await import('jsencript')
// expor default 導出的模塊
const JSEncrypt = module.default
// 配合 JSEncrypt 加密
const encrypt = new JSEncrypt()
encrypt.setPublicKey(PUBLIC_KEY)
const encrypted = encrypt.encrypt(value)
return encrypted
}
複製代碼
import()
相對於 loadScript 來講,更方便的一點是,你一樣能夠用來懶加載你項目中的代碼,或者是 JSON 文件等,由於經過 import()
方式懶加載的代碼或者 JSON 文件,一樣會通過 webpack
處理web
例如項目中用到了城市列表,可是後端並無提供這個 API,而後網上找了一個 JSON 文件,卻並不能經過 loadScript
懶加載把他引入,這個時候就能夠選擇 import()
npm
const module = await import('./city.json')
console.log(module.default)
複製代碼
這些懶加載的優化手段有不少可使用場景,好比渲染 markdown 時用到的 markdown-it
和 highlight.js
,這兩個包加起來是很是大的,徹底能夠在須要渲染的時候使用懶加載的方式引入
有了腳本懶加載,那麼同理可得.....CSS 懶加載
/** * 動態加載樣式 * @param url 樣式地址 */
export function loadStyleSheet(url: string) {
return new Promise((resolve, reject) => {
const matched = Array.prototype.find.call(document.styleSheets, (styleSheet: HTMLLinkElement) => {
return styleSheet.href === url
})
if (matched) {
return resolve()
}
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = url
link.onload = resolve
link.onerror = reject
document.head.appendChild(link)
})
}
複製代碼
路由懶加載也算是老生常談的一個優化手段了,這裏很少介紹,簡單寫一下
function lazyload(loader: () => Promise<{ default: React.ComponentType<any> }>) {
const LazyComponent = React.lazy(loader)
const Lazyload: React.FC = (props: any) => {
return (
<React.Suspense fallback={<Spinner/>}>
<LazyComponent {...props}/>
</React.Suspense>
)
}
return Lazyload
}
const Login = lazyload(() => import('src/pages/Home'))
複製代碼
CDN 可講的也很少,大概就是根據請求的 IP 分配一個最近的緩存服務器 IP,讓客戶端去就近獲取資源,從而實現加速
提及首屏優化,不得不提的一個就是服務端優化。如今的 SPA 應用是利用 JS 腳原本渲染。在腳本執行完以前,用戶看到的會是空白頁面,體驗很是很差。
服務端渲染的原理:
react-dom/server
中的 renderToString
方法將 jsx
代碼轉爲 HTML 字符串,而後將 HTML 字符串返回給瀏覽器ReactDOM.hydrate
進行 "激活",就是將整個邏輯在瀏覽器再跑一遍,爲應用添加點擊事件等交互服務端渲染的大概過程就是上面說的,可是第一步說的,服務端只是將 HTML 字符串返回給了瀏覽器。咱們並無爲它注入 JS 代碼,那麼第三步就完成不了了,沒法在瀏覽器端運行。
因此在第一步以前須要一些準備工做,好比將應用代碼打包成兩份,一份跑在 Node 服務端,一份跑在瀏覽器端。具體的過程這裏就不描述了,有興趣的能夠看我寫的另外一篇文章: TypeScript + Webpack + Koa 搭建自定義的 React 服務端渲染
順便安利一下寫的一個服務端渲染庫:server-renderer
這個並不算是懶加載,只能說算不阻礙主要的任務運行,對加快首屏渲染多多少少有點意思,略過。
有對 webpack 打包生成後的文件進行分析過的小夥伴們確定都清楚,咱們的代碼可能只佔所有大小的 1/10 不到,更多的仍是第三方庫致使了整個體積變大
咱們安裝第三方庫的時候,只是執行npm install xxx
便可,可是他的整個文件大小咱們是不清楚的,這裏安利一下網站: bundlephobia.com
能夠看到,只要輸入 npm 包名稱,就能夠看到使用的 npm 包通過壓縮和 Gzip 後的文件大小。這裏能夠看到,咱們通過使用的 moment 大小既然達到了 65.9 KB!!可是咱們可能只會用到 moment().format(template)
等爲數很少的方法
因此這是一個性價比很是低的庫
可是你把 bundlephobia 拉倒底部的時候,會發現,他會給你推薦一些相似的包
好比 dayjs
既然只要 2.76KB,而且經過他的簡介能夠看出,它提供了和 moment 一個的 API,也就是說,大部分狀況下,你能夠使用 dayjs 代替 moment
若是項目中大量引入了 moment,不容易替換的話,也可使用 webpack
配置解決
const webpackConfig = {
resolve: {
alias: {
moment: 'dayjs',
}
}
}
複製代碼
而後咱們只是換了一個 npm 包,就將大小減小了 60 KB 左右
這部分可能不少人有不一樣的意見,不認同的小夥伴能夠跳過
先說明我對
antd
沒意見,我也很喜歡這個強大的組件庫
antd
對於不少 React 開發的小夥伴來講,多是一個必不可少的配置,由於他方便、強大
可是咱們先看一下他的大小
587.9 KB!這對於前端來講是一個很是大的數字,官方推薦使用 babel-plugin-import
來進行按需引入,可是你會發現,有時候你只是引入了一個 Button
,整個打包的體積增長了200 KB
這是由於它並無對 Icon 進行按需加載,它不能肯定你項目中用到了那些 Icon,因此默認將全部的 Icon 打包進你的項目中,對於沒有用到的文件來講,讓用戶加載這部分資源是一個極大的浪費
像 antd
這類 組件庫是一個很是全面強大的組件庫,像 Select
組件,它提供了很是全面的用法,可是咱們並不會用到全部功能,沒有用到的對於咱們來講一樣是一種浪費
可是不否定像 antd
這類組件庫確實能提升咱們的的開發效率
優化 React 的運行時性能,說到底就是減小渲染次數或者是減小 Diff 次數
由一個常見的聊天功能提及,設計以下
在開始編寫以前對它分析一下,不能一股腦的將全部東西放在一個組件裏面完成
import * as React from 'react'
import { useFormInput } from 'src/hooks'
const InputBar: React.FC = () => {
const input = useFormInput('')
return (
<div className='input-bar'> <textarea placeholder='請輸入消息,回車發送' value={input.value} onChange={input.handleChange} /> </div> ) } export default InputBar 複製代碼
import * as React from 'react'
const ConversationHeader: React.FC = () => {
return (
<div className='conversation-header'> <img src='' alt='' /> <h4>聊天對象</h4> </div> ) } export default ConversationHeader 複製代碼
import * as React from 'react'
import ConversationHeader from './Header'
import MessageList from './MessageList'
import InputBar from './InputBar'
const Conversation: React.FC = () => {
const [messages, setMessages] = React.useState([])
const send = () => {
// 發送消息
}
React.useEffect(
() => {
socket.onmessage = () => {
// 處理消息
}
},
[]
)
return (
<div className='conversation'>
<ConversationHeader/>
<MessageList messages={messages}/>
<InputBar send={send}/>
</div>
)
}
export default Conversation
複製代碼
這樣子不知不覺中,三個組件的分工其實也比較明確了
可是咱們會發現,外層的父組件中的 messages 更新,一樣會引發三個子組件的更新
那麼如何進一步優化呢,就須要結合 React.memo
了
React.memo 和 PureComponent 有點相似,React.memo 會對 props 的變化作一個淺比較,來避免因爲 props 更新引起的沒必要要的性能消耗
咱們就能夠結合 React.memo
修改一下
// 其餘的同理
export default React.memo(ConversationHeader)
複製代碼
而後咱們接着看一下 React.memo
的定義
function memo<T extends ComponentType<any>>( Component: T, propsAreEqual?: (prevProps: Readonly<ComponentProps<T>>, nextProps: Readonly<ComponentProps<T>>) => boolean ): MemoExoticComponent<T>; 複製代碼
能夠看到,它支持咱們傳入第二個參數 propsAreEqual
,能夠由這個方法讓咱們手動對比先後 props 來決定更新與否
export default React.memo(MessageList, (prevProps, nextProps) => {
// 簡單的對比演示,當新舊消息長度不同時,咱們更新 MessageList
return prevProps.messages.length === nextProps.messages.length
})
複製代碼
另外,由於 React.memo
會對先後 props 作淺比較,那此對於咱們很清楚業務中有絕對能夠不更新的組件,儘管他會接受不少 props,咱們想連淺比較的消耗的避過的話,就能夠傳入一個返回值爲 true 的函數
const propsAreEqual = () => true
React.memo(Component, propsAreEqual)
複製代碼
若是會被大量使用的話,咱們就抽成一個函數
export function withImmutable<T extends React.ComponentType<any>>(Component: T) {
return React.memo(Component, () => true)
}
複製代碼
分離靜態不更新組件,減小性能消耗,這部分其實跟 Vue 3.0 的 靜態樹提高 相似
雖然利用 React.memo
能夠避免重複渲染,可是它是針對 props 變化避免的
可是因爲自身 state
或者 context
引發的沒必要要更新,就能夠運用 useMemo
和 useCallback
進行分析優化
由於 Hooks 出來後,咱們大多使用函數組件(Function Component)
的方式編寫組件
const FunctionComponent: React.FC = () => {
// 層架複雜的對象
const data = {
// ...
}
const callback = () => {}
return (
<Child data={data} callback={callback} /> ) } 複製代碼
所以在函數組件的內部,每次更新都會從新走一遍函數內部的邏輯,在上面的例子中,就是一次次建立 data
和 callback
那麼在使用 data
的子組件中,因爲 data 層級複雜,雖然裏面的值可能沒有變化,可是因爲淺比較的緣故,依然會致使子組件一次次的更新,形成性能浪費
一樣的,在組件中每次渲染都建立一個複雜的組件,也是一個浪費,這時候咱們就可使用 useMemo
進行優化
const FunctionComponent: React.FC = () => {
// 層架複雜的對象
const data = React.memo(
() => {
return {
// ...
}
},
[inputs]
)
const callback = () => {}
return (
<Child data={data} callback={callback} /> ) } 複製代碼
這樣子的話,就能夠根據 inputs
來決定是否從新計算 data
,避免性能消耗
在上面用 React.memo
優化的例子,也可使用 useMemo
進行改造
const ConversationHeader: React.FC = () => {
return React.useMemo(() => {
return (
<div className='conversation-header'> <img src='' /> <h4>專業法幣交易</h4> </div> ) }, []) } export default ConversationHeader 複製代碼
像上面說的,useMemo
相對於 React.memo
更好的是,能夠規避 state
和 context
引起的更新
可是 useMemo
和 useCallback
一樣有性能損耗,並且每次渲染都會在 useMemo
和 useCallback
內部重複的建立新的函數,這個時候如何取捨?
useMemo
useCallback 同理....
本文爲邊想邊寫,可能有地方不對,能夠指出
還有一些優化,或者跟業務相連比較精密的優化,可能給忽略了,下次想起來了再整理分享出來
感謝閱讀!