frejs / fre

:ghost: Tiny Concurrent UI library with Fiber.
https://fre.deno.dev
MIT License
3.71k stars 351 forks source link

About Fre exact-updating(FEU) #120

Closed yisar closed 4 years ago

yisar commented 4 years ago

My English is not good, but I try to make a summary and explanation through this article.

What is?

Fre exact-updating(FEU) is a mechanism that make component rerender exact.

https://vuejs.org/v2/guide/comparison.html#Runtime-Performance

https://github.com/vuejs/vue-next/blob/9f52dce0d58f5bc09dded9291eadbb6b1af2dcbe/packages/runtime-core/src/renderer.ts#L871

Questions

  1. performance?

It's not an optimization, It's a different pursuit.

We shouldn't judge it by its performance. React doesn't have it, but its performance won't be very bad. As they say, repeated execution of JS is not worth money.

So I want to say that this mechanism is not about performance. It is to arrange whether the components are updated or not in the framework core.

  1. Can I use useMemo or react.memo or a HOC instead of it?

No.

I'm sorry, but I have to deny it.

This is the view of react all the time. The purpose of FEU is to arrange the update within the framework core, rather than solve the performance problems through other APIs.

I think react's top-down rendering mechanism was wrong from the beginning, especially in the framework of hooks APIs.

Changing it should be a framework, not developers.

  1. rules and limits?

To be honest, there are not many rules, and there will be no more mental overhead.

Just three rules

props with Shallow comparison

state with === comparison

deps with array comparison

As long as you are familiar with these three rules, you can reduce most repeated rendering.

  1. third party libraries?

This mechanism make most of the third-party libraries unusable.

We don't need to be compatible with react. It's a job that costs more money and does less work. Moreover, fre need to change its thinking and find different ways.

Todo

I'm happy that this mechanism will lead to discussion.

In fact, this is the most important mechanism of fre, because it is completely different from react. In the past, I have been plagiarizing from react, but from now on, fre has to go its own way.

Let's look forward to.

mindplay-dk commented 4 years ago

Let's assume you could actually teach every developer to write pure functions with no side effects, no dependencies on global or shared state.

Even if I thought this was a good idea, the current approach just doesn't really work - the optimization you want isn't really possible because comparisons are to unreliable.

If there's an array, it doesn't work.

If there's an event handler, it doesn't work.

If there's a state management library and objects get copied, it doesn't work.

No matter how much you like this idea, at least the way it's implemented now, it doesn't really work - just maybe some of the time.

If this was a reliable optimization that actually worked consistently, I might be able to see the advantage - but with the limitations on it right now, even if I thought this was a good idea, it doesn't really have much practical value.

You seem to be obsessing about a feature that, as far as we know, can't really be implemented reliably in JavaScript, and it's current form only provides a theoretical advantage in some marginal cases.

Show me ideas for actually making this work reliably, and I might be interested? 🤷‍♂️

mindplay-dk commented 4 years ago

Let me just add that, even if developers understood precisely how this worked, and knew how to design components with boundaries and prop types to take advantage of this, that is the last thing you want: components with APIs designed not to make sense to developers, not to make sense for the use cases, but with boundaries and props designed to satisfy technical constraints of the framework's internal optimizations... This would lead to very poor architecture in your projects.

Again, if you could make this optimization work reliably, and people could regard this optimization as an implementation detail, or didn't even need to know about it, I might feel differently.

Implementation details should not "leak" in the sense that people start designing their code around them. An unreliable optimization will encourage them to do that.

yisar commented 4 years ago

No matter how much you like this idea, at least the way it's implemented now, it doesn't really work - just maybe some of the time. Show me ideas for actually making this work reliably, and I might be interested? 🤷‍♂️

vue shouldUpdateComponent

vue hasPropsChanged

My current code is the same as Vue. The shallow comparison rule is almost the same. Only when the length of two objects is different from a certain value, the component will re render.

Maybe we should rearrange this rule to make it more suitable for fre.

Implementation details should not "leak" in the sense that people start designing their code around them. An unreliable optimization will encourage them to do that.

Yes, Vue does not need to consider more cases because of the limitation of templates. For example, the testupdates method in our test is a forEach update. templates does not have this case

yisar commented 4 years ago

In fact, there is only one problem that developers encounter: what should rerender, it will not rerender.

Instead, if they want to not rerender, they just need to use usememo and usecallback.

 const handler= useCallback(fn)
 const arr = useMemo(()=>[])
<Component handler={handler} arr={arr}/> // not rerender
mindplay-dk commented 4 years ago

Instead, if they want to not rerender, they just need to use usememo and usecallback.

There is no obvious reason for anyone to write code like that.

This is just another example of implementation details in the library impacting the architecture of your code: from reading snippet, without knowledge of implementation details in the library, there is no clear reason why this optimization would work.

This is also yet another one of those problems that wouldn't exist in the first place, if we had a distinct initialization phase, where we could generate event handler instances once.

Besides, memoization of an array, as shown in this example, is very unlikely to work for most real-world use-cases. If you have an array, it's typically coming from somewhere outside the component itself - it's usually data being passed into the component from a store, or a parent component, etc.

The reason I don't like this feature, is it will never be obvious to a developer if their component is going to update or not - they will be guessing or trying hard to figure it out, writing extra code to memoize callbacks or values, and then likely using log-statements or something to double-check.

It's just too unreliable, I think.

yisar commented 4 years ago

@mindplay-dk I can't have any ideas at the moment, but I'd like to know why users of Vue can receive this , but we can't?

yisar commented 4 years ago

One thing I want to say, this is the core problem of react or fre.

As you mentioned, the shallow comparison of props is built into the framework, which is not reliable if develops does not understand the comparison rules.

But the deps array comparison of hooks is the same. If you are not familiar with its comparison rules, you may fail to optimize hooks, which is also unreliable

I think both props comparison and deps comparison are similar.

They all have mental burdens, but there is no good solution at present.

This is the article here: https://zhuanlan.zhihu.com/p/98554943 (you can try to translate with Google)

All in all, both deps and props are facing the same problem. which is caused by JavaScript and hooks closure design. The only way to solve this problem is to increase the mental burden of developers.

This will not lead to instability of the framework. What the framework can do is to ensure that the rendering results are correct whether the comparison is effective or not.

At present, there is no good solution.

We can temporarily shift our work, while doing other work, while waiting for the echo of the community.

mindplay-dk commented 4 years ago

I'd like to know why users of Vue can receive this , but we can't?

From my superficial understanding of Vue...

Vue uses a compiler to generate a reactive data structure - so if you start with data like, say:

const data = {
  newTodoText: '',
  visitCount: 0,
  hideCompletedTodos: false,
  todos: [],
  error: null
}

const app = new Vue({ data })

The model is based around mutability of individual props - so, if you say:

app.visitCount += 1

The values of todos, for example, remains unchanged.

With a model based on reactivity and subscriptions, this is quite different from the functional approach of a React-style engine, where you're always dealing with complete sets of data/props during an update.

I don't know Vue very well, but as I understand it, the reactive model is quite different from the React-style model, which is more functional than reactive.

I'm not an expert on this subject, but that's my understanding.

voe uses a reactive decorator to adapt values to a reactivity model, so I would assume you understand this much better than I do. 😉

If you are not familiar with its comparison rules, you may fail to optimize hooks, which is also unreliable

The difference is, this is code the user writes - it's reasonable to expect the user to know the meaning of the arguments they're passing to a call they wrote.

It's less reasonable to expect the user to know about internal implementation details of the engine. The user doesn't get a choice - all props are implicitly compared using this rather unreliable comparison.

As a user, instead of thinking about optimization when I'm choosing to optimize, now I have to think about optimization and implementation details of the engine with every line of code I write - this is a distraction at best (for those who understand the internals of the engine) and at worst (for those who don't) this makes the performance of the engine completely unpredictable.

What I mean by "unreliable" is, if I just write my app, if I focus on the functionality of the thing I'm building, making readable and functional code, without concerning myself with work-arounds for the limitations of prop comparisons internally in the engine, this optimization isn't likely to work.

With any framework, your default mode should be readability and functionality - you shouldn't optimize (especially at the cost of readability!) until there's a need for optimization. For example, memoizing every event-handler is a work-around and a distraction that complicates the code - being encouraged to constantly think about and repeatedly implement specific patterns to work around engine internals, this takes away from productivity and leads to less readable and maintainable code.

As said, I wouldn't be opposed to this, if it were more reliable.

I think the ivi-style component signature (component) => (props) => VDOM might be one way to make this optimization more reliable, as users will naturally write event-handlers, initializations and effects that get declared only once, and therefore can be compared, without having to hand-write memoizations or other work-arounds for engine internals.

I know that's quite a departure from the current model, but I think an optimization like this makes more sense in the context of something like Vue or voe - I don't think this optimization makes a lot of sense with the more functional approach in this project.

What's important to me, is when you're collaborating on a project with a team, reading somebody else's code should make sense, without having to educate the entire team in the implementation details of the engine - people should be able to write readable code with no distracting artifacts, be productive and focus on functionality.

In my opinion, if you're optimizing all your code for prop comparisons, this kind of optimization is a distraction - and if you're not, it's mostly useless overhead and complexity you don't need.

That's a lose/lose proposition, in my opinion.

yisar commented 4 years ago

@mindplay-dk Maybe you are right. If there is an alternative, maybe add an API:

const MemoComponent = Fre.memo(Compoent)

or

<Memo>
  <Component/>
</Memo>

the Componet will compare porps which was memod Which do you think is better?

mindplay-dk commented 4 years ago

I think memoization is a solved problem. Why do we need a different API?

fantasticsoul commented 4 years ago

about exact updating, here is an demo: https://stackblitz.com/edit/concent-todolist-render-key?file=TodoList.js

with logic renderKey, we can easily resolve this problem, recently I may do some work to let cocnent with fre, but the biggest problem is concent is two big(18kb minzip size) compare with fre, so I do know it is worth to try or not .......................

yisar commented 4 years ago

@fantasticsoul Now I have implemented the exact updating, but I want to refactor or remove it in the future. This part will be completely in fre2, and I will spend more time researching new update queue and new scheduler.

yisar commented 4 years ago

In addition, I really need a reason why the same props comparison, Vue will not be opposed.

I'm familiar with the source code of Vue, but I can't find the answer.

mindplay-dk commented 4 years ago

In addition, I really need a reason why the same props comparison, Vue will not be opposed.

I think I explained this above?

The Vue API uses a reactive data-structure - it lets you change individual props; compare this with the React-style API, where only the full set of props can be changed.

I'm familiar with the source code of Vue, but I can't find the answer.

I don't think the answer in the source code, so much as in the concept itself, which is just different.

Either way, I just don't think this is a very good optimization. Because of the functional nature of the de-facto standard JSX/React transform, even if you can stop the actual DOM update from happening, the components have already calculated the JSX nodes - so it's maybe a little better in theory, but it's far from optimal and based on making comparisons that aren't always meaningful or possible in a more functional JavaScript context. The language just wasn't really built for what you're trying to do.

Something like Svelte or Imba gets much closer to optimal by avoiding not only the DOM updates but the calculation of any potential updates entirely. I mentioned some loose ideas earlier, but I don't have a complete idea and I probably didn't explain it very well, but I don't think the Svelte approach (compiling to native JS/DOM manipulation) is the only possible approach to achieving theoretically optimal updates - I think it should be possible to create an alternative JSX-transform where the dependency analysis happens up front and produces a data-structure that allows a library to implement reactivity and optimal updates... I just don't know what such a transform would look like...

I would love it if we could figure out how to not only make smaller optimizations around the shortcomings of the JSX/React-transform, but maybe come up with a new kind of JSX-transform that actually supports atomic updates and/or reactivity.

yisar commented 4 years ago

It will take me a while to figure out the relationship between dependences collection and exact updating

When I have the answer, I'm coming back. If there are relevant articles here, you can share them

yisar commented 4 years ago

Good news, I seem to understand the relationship between accurate updates and dependency collection~

Look at follow code from Mobx:

import React, { Component } from 'react'
import { observable, action } from 'mobx'
import { observer } from 'mobx-react'

class Store {
  @observable count = 0
  @action add() {
    this.count++
  }
}

const store = new Store()

@observer
class Child extends Component {
  render() {
    return <div>{this.props.count}</div>
  }
}

class Parent extends Component {
  render() {
    return (
      <div>
        <button onClick={() => store.add()}>+</button>
        <Chilc count={store.count} />
      </div>
    )
  }
}

export default Parent

When I add @observer to Child, Child will be rerender, and If I add it to Parent, the component is not rerender.

It uses shouldComponentUpdate false to block updates of all subcomponents, and then looks for which component need to be updated through dependency collection, which does not need shallow comparison of props.

Vue's approach is similar. Although it has shallow comparison code, but it doesn't seem necessary.

I now confirm the relationship between dependency collection and exact updating, so I plan to remove it first, and then think about introducing it in a new way.

yisar commented 4 years ago

@mindplay-dk @fantasticsoul Look here https://github.com/yisar/fre/issues/119#issuecomment-577692686, I decided to delete it in the core code and give a new way to solve it. I think this is the best way at present

But the improvement of update queue will continue. I am trying to find a better scheduling scheme. Give me some time.

yisar commented 4 years ago

@mindplay-dk I saw a great idea here: https://github.com/reactjs/rfcs/pull/150 Maybe that is what we want to get.

Detailed research will be done after I finish my graduation project.

yisar commented 4 years ago

Here's a summary, we should expose an API to let users know that props based optimization is working, such as Fre.memo

It should not be built-in https://github.com/yisar/fre/blob/master/demo/src/with-context.js this example is special, in this case, we should use context selector

That should be the next step to do.