每一個前端都想作一個完美的表格,業界也在持續探索不一樣的思路,好比釘釘表格、語雀表格。前端
筆者所在數據中臺團隊也對錶格有着極高的要求,尤爲是自助分析表格,須要兼顧性能與交互功能,本文即是記錄自助分析表格高性能的研發思路。git
要作表格首先要選擇基於 DOM 仍是 Canvas,這是技術選型的第一步。好比釘釘表格就是 基於 Canvas 實現的,固然這不表明 Canvas 實現就比 DOM 實現要好,從技術上各有利弊:github
技術選型要看具體的業務場景,釘釘表格其實就是在線 Excel,Excel 這種形態決定了單元格內必定是簡單文本加一些簡單圖標,所以不用考慮渲染自定義內容的場景,因此選擇 Canvas 渲染在將來也不會遇到很差拓展的麻煩。web
而自助分析表格自然可能拓展圖形、圖片、操做按鈕到單元格中,對軸的拖拽響應交互也很是複雜,爲了避免讓 Canvas 成爲之後拓展的瓶頸,仍是選擇 DOM 實現比較穩當。數組
那問題來了,既然 DOM 渲染效率自然比 Canvas 低,咱們應該如何用 DOM 實現一個高性能表格呢?瀏覽器
其實業界已經有許多 DOM 表格優化方案了,主要以按需渲染、虛擬滾動爲主,即預留一些 Buffer 區域用於滑動時填充,表格僅渲染可視區域與 Buffer 區域部分。但這些方案都不可避免的存在快速滑動時白屏問題,筆者經過不斷嘗試終於發現了一種完美解決的方案,咱們一塊兒往下看吧!微信
即每一個單元格都是用絕對定位的 DIV 實現,整個表格都是有獨立計算位置的 DIV 拼接而成的:框架
這樣作的前提是:性能
帶來的好處是:優化
如圖所示有 16 個單元格,當咱們向右下滑動一格時,中間 3x3 即 9 個格子的區域是徹底不會從新渲染的,這樣零散的絕對定位分佈能夠最大程度維持單元格原本的位置。咱們能夠認爲,任何一格單元格只要自身不超出屏幕範圍,就不會隨着滾動而重渲染。
若是你採用 React 框架來實現,只要將每一個格子的 key 設置爲惟一的便可,好比當前行列號。
通常來講,軸由於邏輯特殊,其渲染邏輯和單元格會分開維護,所以咱們將表格分爲三個區域:橫軸、縱軸、單元格。
顯然,常識是橫軸只能縱向滾動,縱軸只能橫向滾動,單元格能夠橫縱向滾動,那麼橫向和縱向滾動條就只能出如今單元格區域:
這樣會存在三個問題:
.scroll
模擬滾動,這必然會致使單元格與軸滾動有必定錯位,即軸的滾動有幾毫秒的滯後感。overflow: auto
的,而軸區域 overflow: hidden
沒法觸發滾動。通過一番思考,咱們只要將方案稍做調整,就能同時解決上面三個問題:即不要使用原生的滾動條,而是使用 .scroll
代替滾動,用 mousewheel
監聽滾動的觸發:
這樣作帶來什麼變化呢?
.scroll
觸發滾動,使得軸和單元格不會出現錯位,由於軸和單元格都是用 .scroll
觸發的滾動。overflow
屬性。js
控制觸發的滾動發生在渲染完成以後,因此瀏覽器會在滾動發生前現完成渲染,這至關有趣。模擬滾動時,實際上整個表格都是 overflow: hidden
的,瀏覽器就不會給出自帶滾動條了,咱們須要用 DIV 作出虛擬滾動條代替,這個相對容易。
當咱們採用模擬滾動方案時,至關於採用了在滾動時 「高頻渲染」 的方案,所以不須要使用截留,更不要使用 Buffer 區域,由於更大的 Buffer 區域意味着更大的渲染開銷。
當咱們把 Buffer 區域移除時,發現整個屏幕內渲染單元格在 1000 個之內時,現代瀏覽器甚至配合 Windows 都能快速完成滾動前刷新,並不會影響滾動的流暢性。
固然,滾動過快依然不是一件好事,既然滾動是由咱們控制的,能夠稍許控制下滾動速度,控制在每次觸發 mousewheel
位移不超過 200 左右最佳。
像單元格合併、行列隱藏、單元格格式化等計算邏輯,最好在滾動前提早算掉,不然在快速滾動時實時計算必然會帶來額外的計算成本損耗。
可是這種預計算也有弊端,當單元格數量超過 10w 時,計算耗時通常會超過 1 秒,單元格數量超過 100w 時,計算耗時通常會超過 10 秒,用預計算的犧牲換來滾動的流暢,仍是有些遺憾,咱們能夠再思考如下,可否下降預計算的損耗?
局部預計算就是一種解決方案,即使單元格數量有一千萬個,但咱們若是僅計算前 1w 個單元格呢?那不管數據量有多大,都不會出現絲毫卡頓。
但局部預計算有着明顯缺點,即表格渲染過程當中,局部計算結果並不總等價於全局計算結果,典型的有列寬、行高、跨行跨列的計算字段。
咱們須要針對性解決,對於單元格寬高計算,必須採用局部計算,由於全量計算的損耗很是大。但局部計算確定是不許確的,以下圖所示:
但出於性能考慮,咱們初始化可能僅能計算前三行的高度,此時,咱們須要在滾動時作兩件事情:
這樣滾動過程當中雖然單元格會被忽然撐開,但位置並不會產生相對移動,與提早全量撐開後視覺內容相同,所以用戶體驗並不會有實際影響,但計算時間卻由 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 了,不可能出現這種規模的聚合數據。
前端計算還能夠採用多個 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 許可證)