前端使用 Konva 實現視覺化設計器(20)- 效能最佳化、UI 美化

xachary發表於2024-08-08

這一章主要分享一下使用 Konva 遇到的效能最佳化問題,並且介紹一下 UI 美化的思路。

至少有 2 位小夥伴積極反饋,發現本示例有明顯的效能問題,一是記憶體溢位問題,二是卡頓的問題,在這裡感謝大家的提醒。

請大家動動小手,給我一個免費的 Star 吧~

大家如果發現了 Bug,歡迎來提 Issue 喲~

github原始碼

gitee原始碼

示例地址

效能最佳化

記憶體溢位

根據官方文件 Konva Class: Node 的說明:

remove(): remove a node from parent, but don't destroy. You can reuse the node later.
destroy(): remove and destroy a node. Kill it and delete forever! You should not reuse node after destroy().
If the node is a container (Group, Stage or Layer) it will destroy all children too.

在本示例之前的版本中,只使用 remove() 是不正確的,只使用 remove,每次 redraw 都產生巨量的例項沒有被清除,也就是記憶體溢位了,導致 JS heap size 隨隨便便幹到幾個 GB。

【簡單判斷記憶體溢位】
前往:Chrome -> Console 皮膚 -> 左側更多 -> Performance monitor -> JS heap size
如果記憶體只升不降,基本可以認為記憶體溢位了。

在本示例中,大部分圖形例項都是用完即棄的,所以大部分的 remove 都替換為 destory 後,JS heap size 將基本維持在幾十上百 MB(根據內容複雜度)。

這裡提個醒,除了使用 remove 的時候要注意,還有個容易忽略的 API 要注意,就是 Stage、Layer、Group 的 removeChildren(),如果子節點不再有用,建議先遍歷子節點分別 destroy 一下。

初始狀態,如下:

image

卡頓

在本示例之前的版本中,只要畫面需要變化,都是重新 redraw 所有圖形,這導致載入的素材過多的時候,互動會產生明顯的卡頓,尤其是載入 gif 的時候,每一幀都會 redraw 一次。

因此,redraw 必須是可以選擇性 draw 每一層 layer 的,主要調整如下:

// 重繪(可選擇)
  redraw(drawNames?: string[]) {
    const all = [
      Draws.BgDraw.name, // 更新背景
      Draws.LinkDraw.name, // 更新連線
      Draws.AttractDraw.name, // 更新磁貼
      Draws.RulerDraw.name, // 更新比例尺
      Draws.RefLineDraw.name, // 更新參考線
      Draws.PreviewDraw.name, // 更新預覽
      Draws.ContextmenuDraw.name // 更新右鍵選單
    ]

    if (Array.isArray(drawNames) && !this.debug) {
      // 選擇性 draw 也要保持順序
      for (const name of all) {
        if (drawNames.includes(name)) {
          this.draws[name].draw()
        }
      }
    } else {
      for (const name of all) {
        this.draws[name].draw()
      }
    }
  }

這裡有幾點細節考慮:
1、傳哪些 drawNames 就 redraw 哪些 draw 的 group,除非當時是除錯模式。
2、不傳 drawNames 就全 redraw。
3、redraw 要按 all 的順序執行。

舉例:

  • 拖動畫布的時候:
this.render.redraw([Draws.BgDraw.name, Draws.RulerDraw.name, Draws.PreviewDraw.name])

因為這個互動隻影響了 背景、比例尺、預覽的 draw。

  • 放大縮小的時候:
            this.render.redraw([
              Draws.BgDraw.name,
              Draws.LinkDraw.name,
              Draws.RulerDraw.name,
              Draws.RefLineDraw.name,
              Draws.PreviewDraw.name
            ])

此時影響的 draw 就比較多了。

根據不同互動的特點,做必要的 redraw 處理,就可以很好的提高互動效能,減少卡頓。

UI 美化

之前的重心都放在畫布的互動上,介面得過且過就行了。

現在基礎架構基本穩定了,是應該美化一下醜陋的 UI 了,簡單美化後:

image

Naive UI

為了快速美化,這裡用 Naive UI,比較清爽。

主要美化了一下 頭部 和 素材 欄:

  • src/components/main-header
  • src/components/asset-bar

這裡就不貼具體程式碼了,比較簡單。

mitt - Emitter

之前是透過配置式,傳入一些 方法 當作事件的 handler,沒法動態訂閱,太不方便了。

這裡改造了一下 Render,使用 mitt 給它賦予 Emitter 能力:

// 略
import mitt, { type Emitter } from 'mitt'
// 略
export class Render {
  // 略
  protected emitter: Emitter<Types.RenderEvents> = mitt()
  on: Emitter<Types.RenderEvents>['on']
  off: Emitter<Types.RenderEvents>['off']
  emit: Emitter<Types.RenderEvents>['emit']
  // 略
  constructor(stageEle: HTMLDivElement, config: Types.RenderConfig) {
    // 略
    this.on = this.emitter.on.bind(this.emitter)
    this.off = this.emitter.off.bind(this.emitter)
    this.emit = this.emitter.emit.bind(this.emitter)
    // 略
  }
}

在外面的元件裡,透過 render 例項,就可以方便訂閱事件,例如:

        props.render?.on('selection-change', (nodes: Konva.Node[]) => {
            selection.value = nodes
        })

Thanks watching~

More Stars please!勾勾手指~

原始碼

gitee原始碼

示例地址

相關文章