關於 React 性能優化和數棧產品中的實踐

来源:https://www.cnblogs.com/dtux/archive/2023/10/24/17784890.html
-Advertisement-
Play Games

我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 本文作者:的盧 引入 在日常開發過程中,我們會使用很多性能優化的 API,比如像使用 memo、useMemo優化組件或者值,再比如使用 shouldComponent ...


我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。

本文作者:的盧

引入

在日常開發過程中,我們會使用很多性能優化的 API,比如像使用 memouseMemo優化組件或者值,再比如使用 shouldComponentUpdate減少組件更新頻次,懶載入等等,都是一些比較好的性能優化方式,今天我將從組件設計、結構上來談一下 React 性能優化以及數棧產品內的實踐。

如何設計組件會有好的性能?

先看下麵一張圖:

file

這是一顆 React 組件樹,App 下麵有三個子組件,分別是 HeaderContentFooter,在 Content組件下麵又分別有 FolderTreeWorkBenchSiderBar三個子組件,現在如果在 WorkBench 中觸發一次更新,那麼 React 會遍歷哪些組件呢?Demo1

file

function FolderTree() {
  console.log('render FolderTree');
  return <p>folderTree</p>;
}

function SiderBar() {
  console.log('render siderBar');
  return <p>i'm SiderBar</p>;
}

export const WorkBenchGrandChild = () => {
  console.log('render WorkBenchGrandChild');
  return <p>i'm WorkBenchGrandChild</p>
};

export const WorkBenchChild = () => {
  console.log('render WorkBenchChild');
  return (
    <>
      <p>i'm WorkBenchChild</p>
      <WorkBenchGrandChild />
    </>
  );
};

function WorkBench() {
  const [num, setNum] = useState<number>(1);
  console.log('render WorkBench');
  return (
    <>
      <input
        value={num}
        onChange={(e) => {
          setNum(+e.target.value || 0);
        }}
      />
      <p>num is {num}</p>
      <WorkBenchChild />
    </>
  );
}


function Content() {
  console.log('render content');
  return (
    <>
      <FolderTree />
      <WorkBench />
      <SiderBar />
    </>
  );
};

function Footer() {
  console.log('render footer');
  return <p>i'm Footer</p>
};


function Header() {
  console.log('render header');
  return <p>i'm Header</p>;
}


// Demo1
function App() {
  // const [, setStr] = useState<string>();
  return (
    <>
      <Header />
      <Content />
      <Footer />
      {/* <input onChange={(e) => { setStr(e.target.value) }} /> */}
    </>
  );
};

file

根據上面斷點和日誌就可以得到下麵的結論:

  1. 子孫組件每觸發一次更新,React都會重新遍歷整顆組件樹

input 輸入數字,引起 updateNum變更狀態後,react-dombeginWorkcurrent由頂層組件依次遍歷

  1. React更新時會過濾掉未變化的組件,達到減少更新的組件數的目的

在更新過程中,雖然 React重新遍歷了組件樹,但 沒有列印沒有變化的 HeaderFooterFolderTreeSiderBar組件內的日誌

  1. 父組件狀態變化,會引起子組件更新

WorkBenchChild屬於 WorkBench的子組件,雖然 WorkBenchChild沒有變化,但仍被重新渲染,列印了輸入日誌,如果更近一步去斷點會發現 WorkBenchChildoldPropsnewProps是不相等的,會觸發 updateFunctionComponent更新。

綜上我們可以得出一個結論,就是 React自身會有一些性能優化的操作,會儘可能只更新變化的組件,比如 Demo1 中 WorkBenchWorkBenchChildWorkBenchGrandChild組件,而會繞開 不變的 HeaderFooter等組件,那麼儘可能的讓 React更新的粒度就是性能優化的方向,既然儘可能只更新變化的組件,那麼如何定義組件是否變化?

如何定義組件是否變化?

React是以數據驅動視圖的單向數據流,核心也就是數據,那麼什麼會影響數據,以及數據的承載方式,有以下幾點:

  • props
  • state
  • context
  • 父組件不變!

父組件與當前組件其實沒有關聯性,放到這裡是因為,上面的例子中 WorkBenchChild組件中沒有 state、props、context,理論上來說就不變,實際上卻重新 render 了,因為 其父組件 WorkBench有狀態的變動,所以這裡也提了一下,在不使用性能優化 API 的前提下,只要保證 props、state、context & 其父組件不變,那麼組件就不變

還是回到剛剛的例子 Demo WorkBench

export const WorkBenchGrandChild = () => {
  console.log('render WorkBenchGrandChild');
  return <p>i'm WorkBenchGrandChild</p>
};

export const WorkBenchChild = () => {
  console.log('render WorkBenchChild');
  return (
    <>
      <p>i'm WorkBenchChild</p>
      <WorkBenchGrandChild />
    </>
  );
};

function WorkBench() {
  const [num, setNum] = useState<number>(1);
  console.log('render WorkBench');
  return (
    <>
      <input
        value={num}
        onChange={(e) => {
          setNum(+e.target.value || 0);
        }}
      />
      <p>num is {num}</p>
      <WorkBenchChild />
    </>
  );
}

export default WorkBench;

看一下這個 demoWorkBench組件有一個 num狀態,還有一個 WorkBenchChild的子組件,沒有狀態,純渲染組件,同時 WorkBenchChild組件也有一個 純渲染組件 WorkBenchGrandChild子組件,當輸入 input改變 num的值時,WorkBenchChild組件 和 WorkBenchGrandChild組件都重新渲染。我們來分析一下在 WorkBench 組件中,它的子組件 WorkBenchChild 自始至終其實都沒有變化,有變化的其實是 WorkBench 中的 狀態,但是就是因為 WorkBench 中的 狀態發生了變化,導致了其子組件也一併更新,這就帶來了一定的性能損耗,找到了問題,那麼就需要解決問題。

如何優化?

使用性能優化 API

export const WorkBenchGrandChild = () => {
  console.log('render WorkBenchGrandChild');
  return <p>i'm WorkBenchGrandChild</p>
};

export const WorkBenchChild = React.memo(() => {
  console.log('render WorkBenchChild');
  return (
    <>
      <p>i'm WorkBenchChild</p>
      <WorkBenchGrandChild />
    </>
  );
});

// Demo WorkBench
function WorkBench() {
  const [num, setNum] = useState<number>(1);
  console.log('render WorkBench');
  return (
    <>
      <input
        value={num}
        onChange={(e) => {
          setNum(+e.target.value || 0);
        }}
      />
      <p>num is {num}</p>
      <WorkBenchChild />
    </>
  );
}

export default WorkBench;

file

file

我們可以使用 React.memo()包裹 WorkBenchChild組件,在其 diff的過程中 props改為淺對比的方式達到性能優化的目的,通過斷點可以知道 通過 memo包裹的組件在 diffoldPropsnewProps仍然不等,進入了 updateSimpleMemoComponent中了,而 updateSimpleMemoComponent 中有個 shallowEqual淺比較方法是結果相等的,因此沒有觸發更新,而是復用了組件。

狀態隔離(將狀態隔離到子組件中)

function ExchangeComp() {
  const [num, setNum] = useState<number>(1);
  console.log('render ExchangeComp');
  return (
    <>
      <input
        value={num}
        onChange={(e) => {
          setNum(+e.target.value || 0);
        }}
      />
      <p>num is {num}</p>
    </>
  );
};

// Demo WorkBench
function WorkBench() {
  // const [num, setNum] = useState<number>(1);
  console.log('render WorkBench');
  return (
    <>
      <ExchangeComp />
      <WorkBenchChild />
    </>
  );
}

export default WorkBench;

file

file

上面 Demo1 的結論,父組件更新,會觸發子組件更新,就因為 WorkBench狀態改變,導致 WorkBenhChild也更新了,這個時候可以手動創造條件,讓 WorkBenchChild的父組件也就是 WorkBench組件剝離狀態,沒有狀態改變,這種情況下 WorkBenchChild 滿足了 父組件不變的前提,且沒有 statepropscontext,那麼也能夠達到性能優化的結果。

對比

  1. 結果一樣,都是對 WorkBenchChild進行了優化,在 WorkBench組件更新時, WorkBenchChildWorkBenchGrandChild沒有重新渲染
  2. 出發點不一樣,用 memo 性能優化 API 是直接作用到子組件上面,而狀態隔離是在父組件上面操作,而受益的是其子組件

結論

  1. 只要結構寫的好,性能不會太差
  2. 父組件不變,子組件可能不變

性能優化方向

  1. 找到項目中性能損耗嚴重的組件(節點)

在業務項目中,找到卡頓、崩潰 的組件(節點)

  1. 在根組件(節點)上使用性能優化 API

在根組件上使用的目的就是避免其祖先組件如果沒有做好組件設計會給根組件帶來無效的重覆渲染,因為上面提到的,父組件更新,子組件也會更新

  1. 在其他節點上使用 狀態隔離的方式進行優化

優化祖先組件,避免給子組件造成無效的重覆渲染

總結

我們從 組件結構 和 性能優化 API 上介紹了性能優化的兩種不同的優化方式,在實際項目使用上,也並非使用某一種優化方式,而是多種優化方式結合著來以達到最好的性能

產品中的部分實踐

  1. 將狀態隔離到子組件內部,避免引起不必要的更新

    import React, { useCallback, useEffect, useState } from 'react';
    import { connect } from 'react-redux';
    import type { SelectProps } from 'antd';
    import { Select } from 'antd';
    
    import { fetchBranchApi } from '@/api/project/optionsConfig';
    
    const BranchSelect = (props: SelectProps) => {
    	const [list, setList] = useState<string[]>([]);
    	const [loading, setLoading] = useState<boolean>(false);
    	const { projectId, project, tenantId, ...otherProps } = props;
    	const init = useCallback(async () => {
    		try {
    			setLoading(true);
    			const { code, data } = await fetchBranchApi(params);
    			if (code !== 1) return;
    			setList(data);
    		} catch (err) {
    		} finally {
    			setLoading(false);
    		}
    	}, []);
    	useEffect(() => {
    		init();
    	}, [init]);
    
    	return (
    		<Select
    			showSearch
    			optionFilterProp="children"
    			filterOption={(input, { label }) => {
    				return ((label as string) ?? '')
    					?.toLowerCase?.()
    					.includes?.(input?.toLowerCase?.());
    			}}
    			options={list?.map((value) => ({ label: value, value }))}
    			loading={loading}
    			placeholder="請選擇代碼分支"
    			{...otherProps}
    			/>
    	);
    };
    
    export default React.memo(BranchSelect);
    

    比如在中後臺系統中很多表單型組件 SelectTreeSelectCheckbox,其展示的數據需要通過介面獲取,那麼此時,如果將獲取數據的操作放到父組件,那麼每次請求數據不僅會導致需要數據的那個表單項組件更新,同時,其他的表單項也會更新,這就有一定的性能損耗,那麼按照上面的例子這樣將其狀態封裝到內部,避免請求數據影響其他組件更新,就可以達到性能優化的目的,一般建議在外層再加上 memo性能優化 API,避免因為外部組件影響內部組件更新。

  2. Canvas render & Svg render

    file

    // 畫一個小十字
    export function createPlus(
    		point: { x: number; y: number },
    		{ radius, lineWidth, fill }: { radius: number; lineWidth: number; fill: string }
    ) {
    		// 豎 橫
    		const colWidth = point.x - (1 / 2) * lineWidth;
    		const colHeight = point.y - (1 / 2) * lineWidth - radius;
    		const colTop = 2 * radius + lineWidth;
    		const colBottom = colHeight;
    		const rowWidth = point.x - (1 / 2) * lineWidth - radius;
    		const rowHeight = point.y - (1 / 2) * lineWidth;
    		const rowRight = 2 * radius + lineWidth;
    		const rowLeft = rowWidth;
    		return `
    				<path d="M${colWidth} ${colHeight}h${lineWidth}v${colTop}h-${lineWidth}V${colBottom}z" fill="${fill}"></path>
    				<path d="M${rowWidth} ${rowHeight}h${rowRight}v${lineWidth}H${rowLeft}v-${lineWidth}z" fill="${fill}"></path>
    		`;
    }
    
    
    renderPlusSvg = throttle(() => {
    	const plusBackground = document.getElementById(`plusBackground_${this.randomKey}`);
    	const { scrollTop, scrollLeft, clientHeight, clientWidth } = this._container || {};
    	const minWidth = scrollLeft;
    	const maxWidth = minWidth + clientWidth;
    	const minHeight = scrollTop;
    	const maxHeight = minHeight + clientHeight;
    	const stepping = 30;
    	const radius = 3;
    	const fillColor = '#EBECF0';
    	const lineWidth = 1;
    	let innerHtml = '';
    	try {
    		// 根據滾動情況拿到容器的四個坐標點, 只渲染當前滾動容器內的十字,實時渲染
    		for (let x = minWidth; x < maxWidth; x += stepping) {
    			for (let y = minHeight; y < maxHeight; y += stepping) {
    				// 畫十字
    				innerHtml += createPlus({ x, y }, { radius, fill: fillColor, lineWidth });
    			}
    		}
    		plusBackground.innerHTML = innerHtml;
    	} catch (e) {}
    });
    

    問題源於在大數據情況下,由 canvas 渲染的 小十字背景渲染失敗,經測試,業務數據在 200條左右 canvas 畫布繪製寬度就已經達到了 70000px,需要渲染的小十字 數量級在 10w 左右,canvas 不適合繪製尺寸過大的場景(超過某個閥值就會出現渲染失敗,具體閥值跟瀏覽器有關係),而 svg 不適合繪製數量過多的場景,目前的業務場景卻是 畫布尺寸大,繪製元素多,後面的解決方式就是 採用 svg 渲染,將 畫布渲染出來,同時監聽容器的滾動事件,同時只渲染滾動容器中可視區域內的背景,實時渲染,渲染數量在 100 左右,實測就無卡頓現象,問題解決

參考:

  1. React 性能優化的一切
  2. React 源碼解析之 Fiber渲染
  3. 魔術師卡頌

最後

歡迎關註【袋鼠雲數棧UED團隊】~
袋鼠雲數棧UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 如今,大規模、高時效、智能化數據處理已是“剛需”,企業需要更強大的數據平臺,來應對數據查詢、數據處理、數據挖掘、數據展示以及多種計算模型並行的挑戰,湖倉一體方案應運而生。 《實時湖倉實踐五講》是袋鼠雲打造的系列直播活動,將圍繞實時湖倉的建設趨勢和通用問題,邀請奮戰於企業數字化一線的核心產品&技術專家 ...
  • 通過實時索引、查詢和全文搜索引擎,Redis Enterprise提供了更好的數據檢索解決方案。通過強大的搜索引擎助力,Redis Enterprise能在亞毫秒級的時間內提供結果,以增強客戶體驗並助力商業智能。 ...
  • 本文分享自華為雲社區《GaussDB(DWS)性能調優:實時場景下表行數估算不准確引起的的性能瓶頸問題案例》,作者: O泡果奶~。 本文針對實時場景下SQL語句因表行數估算不准確而導致語句執行超時報錯的案例進行分析。 1、【問題描述】 實時場景下,select查詢語句執行時間過長,該語句verbos ...
  • 數據泄露對企業的影響是嚴重的,包括商業機密泄露、法律責任和信譽喪失。為了降低數據泄露的風險,NineData推出了SQL開發規範和用戶訪問量管理功能。用戶訪問量管理功能可以根據用戶的職責和工作需求,靈活配置訪問量,並對特定用戶單獨配置訪問量,並設置到期時間。這個功能適用於數據安全、法律合規和應急響應... ...
  • 1、華為官網介紹 2、OpenHarmony開源項目 3、技術架構 內核層 內核子系統:採用多內核(Linux內核或者LiteOS)設計,支持針對不同資源受限設備選用適合的OS內核 驅動子系統:驅動框架(HDF)是系統硬體生態開放的基礎,提供統一外設訪問能力和驅動開發、管理框架。 系統服務層 系統服 ...
  • 1 它是什麼(協程 和 Kotlin協程) 1.1 協程是什麼 維基百科:協程,英文Coroutine [kəru’tin] (可入廳),是電腦程式的一類組件,推廣了協作式多任務的子程式,允許執行被掛起與被恢復。 作為Google欽定的Android開發首選語言Kotlin,協程並不是 Kotli ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 在前端開發中,有時候一些業務場景中,我們有需求要去實現excel的預覽和列印功能,本文在vue3中如何實現Excel文件的預覽和列印。 預覽excel 關於實現excel文檔線上預覽的做法,一種方式是通過講文檔里的數據處理成html ...
  • 在js中,js變數和JSON是兩個不同數據格式,兩者的儲存方式自然不相同。JSON格式是一種數據交換的規則,js變數則是javascript在程式需求場景中的數據表示。在js與不同語言的服務端進行數據交換過程中,js能夠有內置的方法將其變數轉化為JSON格式。 JSON.parse(data);// ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...