wechat-miniprogram / glass-easel

Multiple-backend component-based JavaScript framework
MIT License
233 stars 34 forks source link

自定义backend遇到的一些困惑 #198

Open fanyipin opened 1 month ago

fanyipin commented 1 month ago

我想做的事情大概是这样:在electron中自定义backend,模式为shadowroot,其中两个webview,service webview运行glass-easel,view webview通过接收service webview的消息进行对dom节点进行绘制,两个webview间通过ipc通信。 在实施过程中,发现如果有1000个循环节点的发,要发送14000多次消息,渲染延迟1-2s。我理解是不是某些service的动作不需要通信消耗,这块有什么好建议吗?感觉现在通信消耗大大的延缓了界面的绘制与响应。 或者这个方向可行吗

fanyipin commented 1 month ago

我设想的是service层在backend的方法例如 appendchild,replacechild等方法中发送消息到view层,view层利用原生的appendchild等方式实现

Tidyzq commented 1 month ago

这个方向是可行的,但是不能直接将 shadow 协议的后端操作对应到原生方法上。 可以参考这个实现 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/src/backend.ts

LastLeaf commented 1 month ago

我没尝试过这么做,不过根据经验可以有两个猜测。

fanyipin commented 1 month ago

这个方向是可行的,但是不能直接将 shadow 协议的后端操作对应到原生方法上。 可以参考这个实现 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/src/backend.ts

嗯,好的,我先学习一下

fanyipin commented 1 month ago

我没尝试过这么做,不过根据经验可以有两个猜测。

  • 通信本身的延迟太大:这个需要你优化一下通信协议,特别是要注意下是不是需要做一些“消息合并”之类的。
  • 界面更新开销太大:最终对 DOM 树的变更应该尽可能在更少的 js task 中完成,换句话说,一次 setData 调用可能会产生很多 backend 调用,这些 backend 调用应当在同一个 js task 中调用到 DOM 。

嗯,消息合并之类的操作已经做过了。后面的dom树变更优化我再想下

还有我看glass-easel要在10月1号完成 MiniProgram 环境下的 webview 后端,这个任务是指将service和view拆分到两个webview去渲染吗?是跟我上面提的事儿类似吗

fanyipin commented 1 month ago

这个方向是可行的,但是不能直接将 shadow 协议的后端操作对应到原生方法上。 可以参考这个实现 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/src/backend.ts

@Tidyzq 抱歉,没有完全看懂这段代码,和我理解的有点儿不太一样,有几个小疑惑希望能帮忙解答一下:

  1. 目前看来也是将backend的每一个动作都要进行通信吗?只不过是每一个动作并不对应一个document的原生动作?
  2. view_controller的作用不是特别明白,可以展开讲讲不?
  3. 这个backend的实现有验证过性能吗?我简单的试了下,如果是循环生成300个列表(列表为文本),在service即backend动作发送消息的时间间隔就已经达到3-4百毫秒级了(这里只统计一次渲染的消息发送间隔),这样的话性能不会受影响吗?当然也有可能是我用法不对,可以帮忙提供一个简单的使用示例吗?
LastLeaf commented 1 month ago

我没尝试过这么做,不过根据经验可以有两个猜测。

  • 通信本身的延迟太大:这个需要你优化一下通信协议,特别是要注意下是不是需要做一些“消息合并”之类的。
  • 界面更新开销太大:最终对 DOM 树的变更应该尽可能在更少的 js task 中完成,换句话说,一次 setData 调用可能会产生很多 backend 调用,这些 backend 调用应当在同一个 js task 中调用到 DOM 。

嗯,消息合并之类的操作已经做过了。后面的dom树变更优化我再想下

还有我看glass-easel要在10月1号完成 MiniProgram 环境下的 webview 后端,这个任务是指将service和view拆分到两个webview去渲染吗?是跟我上面提的事儿类似吗

milestone 的 deadline 会根据实际情况调整,并不准确。 glass-easel 的 milestone 通常是项目自身的目标,和正式 landing 到 MiniProgram 环境也不是一回事。 milestone v1.0 的主要目标是接口全面稳定。

fanyipin commented 1 month ago

@Tidyzq 我直接引用了https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/src/backend.ts里面的代码,并把channel替换成了electron的ipc通信,发现还是存在上面的问题: 我的小程序代码大概如下: js:

Page({
  data: {
    showAgain: false,
    num: 1,
    list: ['tesxt', 'fjeijfioef', 'fejfiejofjeojfoie'],
    renderList:  ["1", "2"]
  },
  helloTap() {
    this.setData({
      showAgain: !this.data.showAgain,
      num: new Date().getTime(),
      renderList: []
    })
    this.setData({
      showAgain: !this.data.showAgain,
      num: new Date().getTime(),
      renderList: Array.from({length: 300}, (_, i) => i + parseFloat(Math.random().toString()).toFixed(2))
    })
  },
})

wxml:

<view wx:for="{{renderList}}">hello-{{index}}-{{item}}</view>
<view class="hello" bind:tap="helloTap">
  Hello world! {{num}}
</view>
<wx-progress></wx-progress>

在点击的时候响应很慢,这个消耗貌似不在通信,我看service从接收到点击事件到最后一次的时间间隔都有将近2s的时间,具体可见下图:

https://github.com/user-attachments/assets/610b8a9c-1fb3-4af5-8137-d97a07a02bc8

上图中右侧为service即glass-easel backend运行的webview,在点击时,我会先清空数组,然后再赋值300项,并动态渲染元素,可以看到在backend侧间隔时间就要接近2s,所以是不是我的用法有问题?

我本意是想要用glass-easel用来模拟小程序的整体架构,在electron中,backend运行在service的webview中,另一个view的webview接收service传递的消息,现在发现渲染性能并不能满足需求,是我的打开方式有问题吗?

我理解通过glass-easel可以将逻辑处理和渲染层renderer分开,这样我就可以在service中处理逻辑,view中接收service的消息进行渲染,可目前看效果不能达到预期,我这样的设计思路和glass-easel的架构思路相符吗? @LastLeaf @Tidyzq 有时间还请指导一下

Tidyzq commented 1 month ago
  1. 目前看来也是将backend的每一个动作都要进行通信吗?只不过是每一个动作并不对应一个document的原生动作?
  2. view_controller的作用不是特别明白,可以展开讲讲不?
  3. 这个backend的实现有验证过性能吗?我简单的试了下,如果是循环生成300个列表(列表为文本),在service即backend动作发送消息的时间间隔就已经达到3-4百毫秒级了(这里只统计一次渲染的消息发送间隔),这样的话性能不会受影响吗?当然也有可能是我用法不对,可以帮忙提供一个简单的使用示例吗?

@fanyipin

  1. 通信应该要做合并操作,而不是每个动作都进行一次通讯。简单的你可以每个微任务合并成一个,复杂的应该是根据 setData 以及事件触发做合并。
  2. backend.ts 用于逻辑侧,view_controller.ts 用于渲染侧。这是一个还未完成的模块所以没有完善的指引,大致使用方式是:
    • 在逻辑侧使用 backend.ts 提供的 ShadowDomBackendContext 作为自定义渲染后端。
    • 在逻辑侧使用 MessageChannelDataSide 将后端操作转换为指令流,传入具体的通讯实现。(你应该在这里做合并)
    • 渲染侧需要准备好 glass_easel 环境,一个渲染后端,以及一个挂载点。
    • 在渲染侧使用 view_controller.ts 提供的 MessageChannelViewSide 接收指令流,传入具体的通讯实现。
    • 在渲染侧使用 ViewController 将指令流转换为挂载点上的树操作。 简单用法可以参考模块内的单元测试代码 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/tests/base/env.ts
  3. 用单元测试代码即可验证,验证过没有你说的这么大开销。

在点击的时候响应很慢,这个消耗貌似不在通信,我看service从接收到点击事件到最后一次的时间间隔都有将近2s的时间,具体可见下图:

github-backend.bak.mp4 上图中右侧为service即glass-easel backend运行的webview,在点击时,我会先清空数组,然后再赋值300项,并动态渲染元素,可以看到在backend侧间隔时间就要接近2s,所以是不是我的用法有问题?

从视频上看你没有做通讯合并操作,耗时花费在了单次操作的通讯消耗。你可以尝试每个微任务合并通讯。

  // 参考 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/tests/base/env.ts。
  const createBridge = () => {
    let _cb: ((...args: any[]) => void) | null = null
    const subscribe = (cb: (...args: any[]) => void) => {
      _cb = cb
    }
    let queue = []
    const publish = (args: readonly any[]): void => {
      // 每个微任务合并
      queue.push(args)
      if (queue.length === 1) {
        Promise.resolve().then(() => {
          queue.forEach(args => _cb?.(args))
          queue = []
        })
      }
    }
    return { subscribe, publish }
  }

  const bridgeToView = createBridge()
  const bridgeToData = createBridge()

  const messageChannelDataSide = MessageChannelDataSide(
    bridgeToView.publish,
    bridgeToData.subscribe,
    getLinearIdGenerator,
  )
  const messageChannelViewSide = MessageChannelViewSide(
    bridgeToData.publish,
    bridgeToView.subscribe,
    syncController,
    getLinearIdGenerator,
  )
fanyipin commented 1 month ago
  1. 目前看来也是将backend的每一个动作都要进行通信吗?只不过是每一个动作并不对应一个document的原生动作?
  2. view_controller的作用不是特别明白,可以展开讲讲不?
  3. 这个backend的实现有验证过性能吗?我简单的试了下,如果是循环生成300个列表(列表为文本),在service即backend动作发送消息的时间间隔就已经达到3-4百毫秒级了(这里只统计一次渲染的消息发送间隔),这样的话性能不会受影响吗?当然也有可能是我用法不对,可以帮忙提供一个简单的使用示例吗?

@fanyipin

  1. 通信应该要做合并操作,而不是每个动作都进行一次通讯。简单的你可以每个微任务合并成一个,复杂的应该是根据 setData 以及事件触发做合并。
  2. backend.ts 用于逻辑侧,view_controller.ts 用于渲染侧。这是一个还未完成的模块所以没有完善的指引,大致使用方式是:

    • 在逻辑侧使用 backend.ts 提供的 ShadowDomBackendContext 作为自定义渲染后端。
    • 在逻辑侧使用 MessageChannelDataSide 将后端操作转换为指令流,传入具体的通讯实现。(你应该在这里做合并)
    • 渲染侧需要准备好 glass_easel 环境,一个渲染后端,以及一个挂载点。
    • 在渲染侧使用 view_controller.ts 提供的 MessageChannelViewSide 接收指令流,传入具体的通讯实现。
    • 在渲染侧使用 ViewController 将指令流转换为挂载点上的树操作。 简单用法可以参考模块内的单元测试代码 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/tests/base/env.ts。
  3. 用单元测试代码即可验证,验证过没有你说的这么大开销。

在点击的时候响应很慢,这个消耗貌似不在通信,我看service从接收到点击事件到最后一次的时间间隔都有将近2s的时间,具体可见下图: github-backend.bak.mp4 上图中右侧为service即glass-easel backend运行的webview,在点击时,我会先清空数组,然后再赋值300项,并动态渲染元素,可以看到在backend侧间隔时间就要接近2s,所以是不是我的用法有问题?

从视频上看你没有做通讯合并操作,耗时花费在了单次操作的通讯消耗。你可以尝试每个微任务合并通讯。

  // 参考 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/tests/base/env.ts。
  const createBridge = () => {
    let _cb: ((...args: any[]) => void) | null = null
    const subscribe = (cb: (...args: any[]) => void) => {
      _cb = cb
    }
    let queue = []
    const publish = (args: readonly any[]): void => {
      // 每个微任务合并
      queue.push(args)
      if (queue.length === 1) {
        Promise.resolve().then(() => {
          queue.forEach(args => _cb?.(args))
          queue = []
        })
      }
    }
    return { subscribe, publish }
  }

  const bridgeToView = createBridge()
  const bridgeToData = createBridge()

  const messageChannelDataSide = MessageChannelDataSide(
    bridgeToView.publish,
    bridgeToData.subscribe,
    getLinearIdGenerator,
  )
  const messageChannelViewSide = MessageChannelViewSide(
    bridgeToData.publish,
    bridgeToView.subscribe,
    syncController,
    getLinearIdGenerator,
  )

嗯,我再尝试下,这个应该也跟我开着devtool + console有关。不过有一点还是不太了解,用户点击事件到service接收响应再到setData目前不都是框架本身处理的吗?我怎么根据setData去做合并呢?我现在了解到的还是在自定义的context中的相关api里实现自定义逻辑