精讀《高性能表格》

每一個前端都想作一個完美的表格,業界也在持續探索不一樣的思路,好比釘釘表格、語雀表格。前端

筆者所在數據中臺團隊也對錶格有着極高的要求,尤爲是自助分析表格,須要兼顧性能與交互功能,本文即是記錄自助分析表格高性能的研發思路。git

精讀

要作表格首先要選擇基於 DOM 仍是 Canvas,這是技術選型的第一步。好比釘釘表格就是 基於 Canvas 實現的,固然這不表明 Canvas 實現就比 DOM 實現要好,從技術上各有利弊:github

  • Canvas 渲染效率比 DOM 高,這是瀏覽器實現致使的。
  • DOM 可拓展性比 Canvas 好,渲染自定義內容首選 DOM 而非 Canvas。

技術選型要看具體的業務場景,釘釘表格其實就是在線 Excel,Excel 這種形態決定了單元格內必定是簡單文本加一些簡單圖標,所以不用考慮渲染自定義內容的場景,因此選擇 Canvas 渲染在將來也不會遇到很差拓展的麻煩。web

而自助分析表格自然可能拓展圖形、圖片、操做按鈕到單元格中,對軸的拖拽響應交互也很是複雜,爲了避免讓 Canvas 成爲之後拓展的瓶頸,仍是選擇 DOM 實現比較穩當。數組

那問題來了,既然 DOM 渲染效率自然比 Canvas 低,咱們應該如何用 DOM 實現一個高性能表格呢?瀏覽器

其實業界已經有許多 DOM 表格優化方案了,主要以按需渲染、虛擬滾動爲主,即預留一些 Buffer 區域用於滑動時填充,表格僅渲染可視區域與 Buffer 區域部分。但這些方案都不可避免的存在快速滑動時白屏問題,筆者經過不斷嘗試終於發現了一種完美解決的方案,咱們一塊兒往下看吧!微信

單元格使用 DIV 絕對定位

即每一個單元格都是用絕對定位的 DIV 實現,整個表格都是有獨立計算位置的 DIV 拼接而成的:框架

這樣作的前提是:性能

  1. 全部單元格位置都要提早計算,這裏能夠利用 web worker 作並行計算。
  2. 單元格合併僅是產生一個更大的單元格,它的定位方式與小單元格並沒有差別。

帶來的好處是:優化

  1. 滾動時,單元格能夠最大程度實現複用。
  2. 對於合併的單元格,只會讓可視區域渲染的總單元格數更小,更利於性能提高,而不是帶來性能負擔。

如圖所示有 16 個單元格,當咱們向右下滑動一格時,中間 3x3 即 9 個格子的區域是徹底不會從新渲染的,這樣零散的絕對定位分佈能夠最大程度維持單元格原本的位置。咱們能夠認爲,任何一格單元格只要自身不超出屏幕範圍,就不會隨着滾動而重渲染。

若是你採用 React 框架來實現,只要將每一個格子的 key 設置爲惟一的便可,好比當前行列號。

模擬滾動而非原生滾動

通常來講,軸由於邏輯特殊,其渲染邏輯和單元格會分開維護,所以咱們將表格分爲三個區域:橫軸、縱軸、單元格。

顯然,常識是橫軸只能縱向滾動,縱軸只能橫向滾動,單元格能夠橫縱向滾動,那麼橫向和縱向滾動條就只能出如今單元格區域:

這樣會存在三個問題:

  1. 單元格使用原生滾動,橫縱軸只能在單元格區域監聽滾動後,經過 .scroll 模擬滾動,這必然會致使單元格與軸滾動有必定錯位,即軸的滾動有幾毫秒的滯後感。
  2. 鼠標放在軸上時沒法滾動,由於只有單元格是 overflow: auto 的,而軸區域 overflow: hidden 沒法觸發滾動。
  3. 快速滾動出現白屏,即使留了 Buffer 區域,在快速滾動時也無能爲力,這是由於渲染速度跟不上滾動致使的。

通過一番思考,咱們只要將方案稍做調整,就能同時解決上面三個問題:即不要使用原生的滾動條,而是使用 .scroll 代替滾動,用 mousewheel 監聽滾動的觸發:

這樣作帶來什麼變化呢?

  1. 軸、單元格區域都使用 .scroll 觸發滾動,使得軸和單元格不會出現錯位,由於軸和單元格都是用 .scroll 觸發的滾動。
  2. 任何位置都能監聽滾動,使得軸上也能滾動了,咱們再也不依賴 overflow 屬性。
  3. 快速滾動時驚喜的發現不會白屏了,緣由是用 js 控制觸發的滾動發生在渲染完成以後,因此瀏覽器會在滾動發生前現完成渲染,這至關有趣。

模擬滾動時,實際上整個表格都是 overflow: hidden 的,瀏覽器就不會給出自帶滾動條了,咱們須要用 DIV 作出虛擬滾動條代替,這個相對容易。

零 buffer 區域

當咱們採用模擬滾動方案時,至關於採用了在滾動時 「高頻渲染」 的方案,所以不須要使用截留,更不要使用 Buffer 區域,由於更大的 Buffer 區域意味着更大的渲染開銷。

當咱們把 Buffer 區域移除時,發現整個屏幕內渲染單元格在 1000 個之內時,現代瀏覽器甚至配合 Windows 都能快速完成滾動前刷新,並不會影響滾動的流暢性。

固然,滾動過快依然不是一件好事,既然滾動是由咱們控制的,能夠稍許控制下滾動速度,控制在每次觸發 mousewheel 位移不超過 200 左右最佳。

預計算

像單元格合併、行列隱藏、單元格格式化等計算邏輯,最好在滾動前提早算掉,不然在快速滾動時實時計算必然會帶來額外的計算成本損耗。

可是這種預計算也有弊端,當單元格數量超過 10w 時,計算耗時通常會超過 1 秒,單元格數量超過 100w 時,計算耗時通常會超過 10 秒,用預計算的犧牲換來滾動的流暢,仍是有些遺憾,咱們能夠再思考如下,可否下降預計算的損耗?

局部預計算

局部預計算就是一種解決方案,即使單元格數量有一千萬個,但咱們若是僅計算前 1w 個單元格呢?那不管數據量有多大,都不會出現絲毫卡頓。

但局部預計算有着明顯缺點,即表格渲染過程當中,局部計算結果並不總等價於全局計算結果,典型的有列寬、行高、跨行跨列的計算字段。

咱們須要針對性解決,對於單元格寬高計算,必須採用局部計算,由於全量計算的損耗很是大。但局部計算確定是不許確的,以下圖所示:

但出於性能考慮,咱們初始化可能僅能計算前三行的高度,此時,咱們須要在滾動時作兩件事情:

  1. 在快速滾動的時候,向 web worker 發送預計要滾動到的位置,增量計算這些位置文字寬度,並實時修正列總寬。(由於列總寬算完只要存儲最大值,因此已計算的數量級會被壓縮爲 O(1))。
  2. 寬度計算完畢後,快速刷新當前屏幕單元格寬度,但在寬度校準的同時,維持可視區域內左對齊不變,以下圖所示:

這樣滾動過程當中雖然單元格會被忽然撐開,但位置並不會產生相對移動,與提早全量撐開後視覺內容相同,所以用戶體驗並不會有實際影響,但計算時間卻由 O(row * column) 降低到 O(1),只要計算一個常數量級的單元格數目。

計算字段也是同理,能夠在滾動時按片預計算,但要注意僅能在計算涉及局部單元格的狀況下進行,若是這個計算是全局性質的,好比排名,那麼局部排序的排名確定是錯誤的,咱們必須進行全量計算。

好在,即使是全量計算,咱們也只須要考慮一部分數據,假設行列數量都是 n,能夠將計算複雜度由 O(n²) 下降爲 O(n):

這種計算字段的處理沒法保證支持無限數量級的數據,但能夠大大下降計算時間,假設 1000w 單元格計算時間開銷是 60s,這是一個幾乎不能忍受的時間,假設 1000w 單元格是 1w 行 * 1k 列造成的,咱們局部計算的開銷是 1w 行(100ms) + 1k 列(10ms) = 0.1s,對用戶來講幾乎感覺不到 1000w 單元格的卡頓。

在 10w 行 * 10w 列的狀況下,等待時間是 1+1 = 2s,用戶會感覺到明顯卡頓,但總單元格數量但是驚人的 100 億,光數據可能就幾 TB 了,不可能出現這種規模的聚合數據。

Map Reduce

前端計算還能夠採用多個 web worker 加速,總之不要讓用戶電腦的 CPU 閒置。咱們能夠經過 window.navigator.hardwareConcurrency 獲取硬件並行能支持的最大 web worker 數量,咱們就實例化等量的 web worker 並行計算。

拿剛纔排名的例子來講,一樣 1000w 單元格數量,若是隻有一列呢?那行數就是紮紮實實的 1000w,這種狀況下,即使 O(n) 複雜度計算耗時也可能突破 60s,此時咱們就能夠分段計算。個人電腦 hardwareConcurrency 值爲 8,那麼就實例化 8 個 web worker,分別並行計算第 0 ~ 125w, 125w ~ 250w ..., 875w ~ 1000w 段的數據分別進行排序,最後獲得 8 段有序序列,在主 worker 線程中進行合併。

咱們能夠採用分治合併,即針對依次收到的排序結果 x1, x2, x3, x4...,將收到的結果兩兩合併成 x12, x34, ...,再次合併爲 x1234 直到合併爲一個數組爲止。

固然,Map Reduce 並不能解決全部問題,假設 1000w 數據計算耗時 60s,咱們分爲 8 段並行,每一段平均耗時 7.5s,那麼第一輪排序總耗時爲 7.5s。分治合併時間複雜度爲 O(kn logk),其中 k 是分段數,這裏是 8 段,logk 約等於 3,每段長度 125w 是 n,那麼一個 125w 數量級的二分排序耗時大概是 4.5s,時間複雜度是 O(n logn),因此等價爲 logn = 4.5s, k x logk 等於幾?這裏因爲 k 遠小於 n,因此時間消耗會遠小於 4.5s,加起來耗時不會超過 10s。

總結

若是你想打造高性能表格,DIV 性能足夠了,只要注意實現的時候稍加技巧便可。你能夠用 DIV 實現一個兼顧性能、拓展性的表格,是時候從新相信 DOM 了!

筆者建議讀完本文的你,按照這樣的思路作一個小 Demo,同時思考,這樣的表格有哪些通用功能能夠抽象?如何設計 API 才能成爲各種業務表格的基座?如何設計功能才能知足業務層表格繁多的拓展訴求?

討論地址是: 精讀《高性能表格》· Issue #309 · dt-fe/weekly

若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

版權聲明:自由轉載-非商用-非衍生-保持署名( 創意共享 3.0 許可證
相關文章
相關標籤/搜索