剖析 Antd Table 組件的錯位和性能問題

零. 開門見山的 Bug 解決方案

  1. 該寫固定的 width,height 就寫固定 width,height 。(修復簡單,但這樣 table 就不靈活,無法針對動態變化高度)
  2. 針對簡單場景,能夠用 setTimeout 在 mounted 以後觸發下文的 syncFixedTableRowHeight。(缺點就是不穩定,可能還須要配套寫檢查資源加載的監聽函數,但減小了出bug的機率)
  3. 使用 ResizerObserver 監聽高度或寬度等屬性變化,同步變化信息,方法見下文。(完好點)

一. 前言 & 復現 Bug

在作活動引擎的過程當中,發現 Antd 的 Table 組件會發送各類行錯位,和列錯位。關於行錯位不是本節內容介紹的重點,本文主要介紹在啓用固定列的時候(即便用fixed) 時發生的列錯位 bug 以及其衍生的一系列性能問題。javascript

下圖是使用固定列和 Image 做爲列內容時產生的現象,該案例很是容易復現。 css

代碼java

import { Table } from 'antd';

const columns = [
  {
    title: 'Full Name',
    width: 100,
    dataIndex: 'name',
    key: 'name',
    fixed: 'left',
  },
  {
    title: 'Age',
    width: 100,
    dataIndex: 'age',
    key: 'age',
    fixed: 'left',
  },
  { title: 'Column 1', dataIndex: 'address', key: '1' },
  { title: 'Column 2', dataIndex: 'address', key: '2' },
  { title: 'Column 3', dataIndex: 'address', key: '3' },
  { title: 'Column 4', dataIndex: 'address', key: '4' },
  { title: 'Column 5', dataIndex: 'address', key: '5' },
   {
    title: 'Avatar',
    width: 200,
    dataIndex: 'img',
    key: 'img',
    render: (a, row ,b) => (
      <img src={row.img}></img>
    )
  },
  { title: 'Column 6', dataIndex: 'address', key: '6' },
  { title: 'Column 7', dataIndex: 'address', key: '7' },
  { title: 'Column 8', dataIndex: 'address', key: '8' },
  {
    title: 'Action',
    key: 'operation',
    fixed: 'right',
    width: 100,
    render: () => <a href="javascript:;">action</a>,
  },
];

const data = [
  {
    key: '1',
    name: 'John Brown',
    age: 32,
    address: 'New York Park',
    img: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566139960708&di=b71fcbe1841e966f5fd3983197628ff9&imgtype=0&src=http%3A%2F%2Fpic1.16xx8.com%2Fallimg%2F161122%2F1F0035M6-7.jpg'
  },
  {
    key: '2',
    name: 'Jim Green',
    age: 40,
    address: 'London Park',
    img: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566139968891&di=c0b7ecc441817226dabc251d870f6d22&imgtype=0&src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F01a949581aeb9fa84a0d304fd05eeb.jpg'
  },
];

ReactDOM.render(<Table columns={columns} dataSource={data} scroll={{ x: 1300 }} />, mountNode);
複製代碼

然而一樣的狀況在之前使用 Element 時幾乎不會復現,因而我使用了 Element-React ,果真 Element-React 不管怎麼玩都不會出現列錯位或者行錯位。瀏覽器

二.將固定列渲染爲 Html

一般來講,主流的固定列渲染方法無一例外都是將固定列的 column 渲染爲單獨的 Table 組件。而後使用絕對定位(position:absolute)將其固定在 Table 的左右兩側。bash

疑問:固定列(Fixed)屬性是如何同步主內容 Table Cell 和左右 Fixed Table Cell 的高度?

絕對佈局會形成 Fixed Table 和 Main Table 之間列元素的 Layout 信息(如 cell height,cell width)割裂。必須使用某種方式同步割裂的信息。 這裏 Element - React 和 Antd 對於 Table 在 fixed 的實現差距是很大的。antd

1.Element 同步 Layout 方案:

同一個 Table 會被渲染成三份,裏面的 Dom 節點,樣式徹底同樣,不一樣的是 Fixed 的 Table 部分是不 Visiable 的。app

能夠說 Element - React 對於的 Table 爲了彌補樣式錯位的問題,巨大地犧牲了性能的問題。這樣的性能問題會在列數和列元素複雜度提高時,表現出來。函數

先看 Element - React 關於 Fixed 屬性的渲染後結構: 佈局

下面用直觀圖來表示這種設計:性能

能夠看到 主Table 和 左右Fixed Table 的結構是如出一轍的,能夠說很浪費了。。。 原本只須要渲染對應的 Table 列就能夠了,但不得不說 Element 的作法的確解決了這個同步 Layout 信息的問題。

2.Antd 同步 Layout 方案:

Antd 的 Table 內核使用了 Rc-table 組件。Rc-table 不會渲染徹底相同的 3 份Table,而是隻渲染須要的 column,能夠看到這種設計纔是合理的。。。

而後雖然設計合理,可是 Antd 卻產生很是多錯位 bug。能夠歸結於 Rc-table 與 其他獨立 Antd 組件之間,發生了一些配合的失誤。這裏依然只瞄準列錯位的 bug。

直接去 Rc-table 的源碼找到了同步固定列 Table 高度的代碼段:

syncFixedTableRowHeight = () => {
    //...
    //搜尋主 Table 全部行元素
    const bodyRows = this.bodyTable.querySelectorAll(`.${prefixCls}-row`) || [];
    //...
    const state = this.store.getState();
    //獲取主 Table 的行高
    const fixedColumnsBodyRowsHeight = [].reduce.call(
      bodyRows,
      (acc, row) => {
        const rowKey = row.getAttribute('data-row-key');
        const height =
          row.getBoundingClientRect().height || state.fixedColumnsBodyRowsHeight[rowKey] || 'auto';
        acc[rowKey] = height;
        return acc;
      },
      {},
    );
    
    //比較是否發生了,若是沒有發生變化,就返回
    if (
      shallowequal(state.fixedColumnsHeadRowsHeight, fixedColumnsHeadRowsHeight) &&
      shallowequal(state.fixedColumnsBodyRowsHeight, fixedColumnsBodyRowsHeight)
    ) {
      return;
    }

    //若是發生了變化,就同步變化
    this.store.setState({
      fixedColumnsHeadRowsHeight,
      fixedColumnsBodyRowsHeight,
    });
};
複製代碼

它會在 mounted 的時候調用,和 document 的 resize 事件調用。

但這裏有一個 bug,mounted 的時候還有不少元素沒有渲染出來時,如 Image。syncFixedTableRowHeight 同步時就不會計算圖片地高度,這樣就會產生高度割裂,雖然觸發了函數,但沒有同步高度的問題。

3.如何修復 rc-table 的列同步 bug

因而瞭解原理以後,這裏天然而然能夠想到給 Image 顯式地指定 css height。這樣一來,在 mounted 地時候就能夠 調用 syncFixedTableRowHeight 獲取高度,即使圖片尚未渲染出來。

完美解決方法是:使用 ResizeObserver,Antd 的幾乎全部錯位問題,幾乎都被這個方法解決了,(也許時由於瀏覽器兼容性,目前這個 PR 躺了大半年了,但我以爲是維護不及時...),即使是兼容性問題,ResizeObserver有基於MutationObserver的polyfill,而主流瀏覽器對MutationObserv是支持的。

下面是解決方案地代碼,之後若是造輪子,能夠參考:

createObserver() {
    return new ResizeObserver(entries => {
      const state = this.store.getState();
	
      const fixedColumnsHeadRowsHeight = { ...state.fixedColumnsHeadRowsHeight };
	
      const fixedColumnsBodyRowsHeight = { ...state.fixedColumnsBodyRowsHeight };
	
      const firstRowCellsWidth = { ...state.firstRowCellsWidth };
	
      for (let i = 0; i < entries.length; i++) {
        const entry = entries[i];
        const { target } = entry;
        const headerRowIndex = target.getAttribute('data-header-row-index')
        const rowKey = target.getAttribute('data-row-key');
        const columnKey = target.getAttribute('data-column-Key')
        const { width, height } = target.getBoundingClientRect();
        
        if (headerRowIndex !== null) {
          if (fixedColumnsHeadRowsHeight[headerRowIndex] !== height) {
            fixedColumnsHeadRowsHeight[headerRowIndex] = height;
          }
        }
        if (rowKey !== null) {
          if (fixedColumnsBodyRowsHeight[rowKey] !== height) {
            fixedColumnsBodyRowsHeight[rowKey] = height;
          }
        }
        if (columnKey !== null) {
          if (
            firstRowCellsWidth[columnKey] === undefined ||
            width !== firstRowCellsWidth[columnKey]
          ) {
            firstRowCellsWidth[columnKey] = width;
          }
        }
      }
      this.store.setState({
        fixedColumnsHeadRowsHeight,
        fixedColumnsBodyRowsHeight,
        firstRowCellsWidth,
      });
    });
  }
複製代碼

既然解決了 Layout 高度的問題,如何解決同步動態屬性呢

這種動態屬性相似於 scrollTop,scrollLeft,hoverCellIndex 等等。 這裏 Element 和 Antd 的實現是如出一轍的。

好比如何同步三個 Table 的 onScroll 屬性,咱們能夠監聽主 Table 的 onScroll 事件。而後將主 Table 的 scrollTop 和 scrollLeft 分發到左右 Fixed table 上。這樣的結果就是引起3次重繪。

syncScroll() {
    const { headerWrapper, footerWrapper, bodyWrapper, fixedBodyWrapper, rightFixedBodyWrapper } = this;
    if (headerWrapper) {
      headerWrapper.scrollLeft = bodyWrapper.scrollLeft;
    }
    if (footerWrapper) {
      footerWrapper.scrollLeft = bodyWrapper.scrollLeft;
    }

    if (fixedBodyWrapper) {
      fixedBodyWrapper.scrollTop = bodyWrapper.scrollTop;
    }
    if (rightFixedBodyWrapper) {
      rightFixedBodyWrapper.scrollTop = bodyWrapper.scrollTop;
    }
  }

// 主 Table

<div
  style={this.bodyWrapperHeight}
  className="el-table__body-wrapper"
  ref={this.bindRef('bodyWrapper')}
  onScroll={this.syncScroll}
>
  <TableBody
    {...this.props}
    style={{ width: this.bodyWidth }}
  />
</div>
複製代碼

性能比較

Element 部分

這裏直接進入性能比較環節,來證實 Element 在固定列上地設計對性能有多麼大地損耗。

下面是從初始化到不斷滾動,觸發 onScroll 重繪的 Performance 截圖。(0 - 10ms)

滾動時

先看調用調用棧和總體流程時間佔比:

這個截圖能夠證實 Element Table 在 Fixed 屬性上的設計是有很大問題,大部分的時間都花費在渲染上,這與其設計有很是大的關係。 假如咱們把 Fixed 屬性拿走,再來測試一下:

能夠發現渲染上的性能差距很是大:

Antd 部分

下面能夠看到 Antd 在 Table 上的表現好的不是一點點。渲染時間很是短,仔細看代碼地話,Antd 和 Rc-table 在一些細節上下了功夫,如 debounce 和 throttle 。這裏不一一列舉了,只大概讀了下代碼,沒有作實驗考證具體優化了多少。

不過既然展開就多說兩句,Antd 在 Table 上的確知足了普適地後臺需求,經過分頁能夠解決大部分性能問題。可是對於不少大數據場景,Antd 地性能實際也是很通常的,沒有對大數據場景做優化。這其中有不少地優化技巧。其中最實用地莫過於虛擬滾動。

Table 設計比較

因爲 Fixed 的解決方案不一樣, 也部分形成了 Table 設計地差別。

React-Element

Table Store 會同時做爲3個 Table 的數據源,甚至這三個 Table 除了可顯示部分不一樣之外,其餘部分都是幾乎相同的。可是可見對 Fixed 的數據特有的屬性沒有不少,代碼總體很簡潔一些,能夠講用空間(指代碼數量)換時間。

Rc-Table

Rc-Table && Antd Table 的關係

最後對 Antd 和 Element 的改進建議

嚴格意義來講是對 Rc-table 的改進建議,但願能夠推進 ResizeObserver 的更新。

對 Element, 但願能夠早點拿走 Fixed 這種嚴重損耗性能的設計。理論上會形成3倍的性能損耗,可是實際在更加複雜的環境下,這種性能損耗會被更加放大。

在 Table 上的設計,Antd 優於 Element ,只不過被 Rc-table 坑了,Rc-table 目前對於維護上比較滯後,老實說但願 Antd 本身實現一套 Table-core 組件。不管是哪個,目前看來都有不小的優化空間。

相關文章
相關標籤/搜索