forgetti
forgetti copied to clipboard
Feature: `forgetti/runtime`
This is a thread discussing the introduction of forgetti/runtime, which would provide APIs that aren't achievable alone by the compiler.
$$equals(a, b)
Object.is is the slowest of the three comparison methods, the other being == and ===. equal provides a short path to allow using referential equality on non-number types.
Ideal implementation would look like:
function equals<T, R>(a: T, b: T) {
return a === b || (a !== a && b !== b);
}
$$cache(useMemo, size)
- This simplifies the output for the root cache.
- Also adds other fields that may be useful (to be discussed in the latter part of the list).
$$effect(effectHook, index, callback, dependency)
- This allows effect hooks to be unlocked from the hook rules.
- Relies on the new
$$cache
$$childCache(size)
- Allows for 3P hooks to unlock its own hook rules by accessing the underlying root or parent cache. Previously it created its own root cache.
$$memo(memoHoc, Component, keys)
- Memoizes components with monomorphic props.
Template splitting
This is still a pending design. The aim is for to split the template part from the logical part, producing a stateless memoized component. The design has the following considerations:
Splitting strategies
Per-element split
This solution aims to split every JSX element to be a standalone memoized component. This is definitely granular, but may consume too much memory
Singular template split
This solution only splits for the top-level JSX element. It has the same memoization process as the first solution except that the entirety is now grouped and is probably cheaper.
Component reference checking
This solution only applies memoization for potentially unmemoized components. The split creates a wrapper component at top-level and uses $$memo. Under the hood, $$memo decides if it should memoized the component by checking some key properties (either checking the component's name via React or assigning a symbol).
New JSX runtime
Maybe worth looking at?
Inspiration:
- https://electricui.com/blog/wishlist-for-a-react-compiler
This proposal looks good! Just some thoughts and questions:
-
In the context of $$childCache, I don't completely understand how accessing the underlying root or parent cache would improve the situation compared to creating a new root cache. Could you provide more information on the benefits of this change and any potential trade-offs?
-
For unlocking effect hooks from the hook rules using the new
$$cache, could you elaborate on the advantages this would provide? Are there any potential drawbacks or scenarios where this may not be desirable? -
Per-element split could just be static hoisting here and it should be better than memo IMO:
const div = <div>foo</div>;
function Component() {
return div
}
-
Introducing a new JSX runtime is an interesting idea. What specific improvements do you envision in this new runtime that would justify the effort required to develop it?
-
On a side note, it's theoretically it's possible to use million optimizations here given that forgetti can produce stateless components. Maybe I will explore this as part of million/react
In the context of $$childCache, I don't completely understand how accessing the underlying root or parent cache would improve the situation compared to creating a new root cache. Could you provide more information on the benefits of this change and any potential trade-offs?
As you know, forgetti generates a root cache at the component which is what powers the memoization process. The thing with this is that it is sophisticated to work for branched statements (if-else) and loop statements (for) by producing a "branched cache", which allows useMemo, useCallback and useRef to be called without being restricted by the hook rules! This is what I call "unlocking" because now the hooks aren't as strict as before.
The problem with other hook-like functions is that right now, forgetti assumes that the hook will still be relying on its rules. On top of that, hooks generate a root cache on their own, essentially "locking" themselves since cache creation relies on useMemo. Like I mentioned with branched caches, ideally forgetti should produce a cache based on its parent cache, rather than creating a cache of its own.
You can read more about it here: https://github.com/lxsmnsyc/forgetti#branched-caching
For unlocking effect hooks from the hook rules using the new $$cache, could you elaborate on the advantages this would provide? Are there any potential drawbacks or scenarios where this may not be desirable?
Currently forgetti relies on calling effect hooks directly, which means it is still "locked". The proposed solution here is to declare an effect once, per kind, and then provide an "array" that would push the pending effects for the effect to run. It would look something like this barely:
const effects = useComposedEffect(useEffect, size);
effects[0] = [myEffect, dependencies];
Per-element split could just be static hoisting here and it should be better than memo IMO:
Yeah this is probably something I missed. Currently forgetti progressively memoizes the JSX expression, but of course for static JSX it is still included in the cache, but yes ideally it should be hoisted as well as literals and guaranteed literals.
Introducing a new JSX runtime is an interesting idea. What specific improvements do you envision in this new runtime that would justify the effort required to develop it?
This one is for pre-wrapping the components by the memo API before being passed to the VDOM object. Funnily enough I think it wouldn't work w/o explicitly defining the memo HOC.
On a side note, it's theoretically it's possible to use million optimizations here given that forgetti can produce stateless components. Maybe I will explore this as part of million/react
Yeah but take note though that I think million relies on serializable data. forgetti already has the memoization step, what forgetti only lacks is the ability to prevent re-rendering that which is only achievable through the memo HOC