Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LiveComponent] deferred/lazy loading #994

Closed
kbond opened this issue Jul 9, 2023 · 9 comments · Fixed by #1143
Closed

[LiveComponent] deferred/lazy loading #994

kbond opened this issue Jul 9, 2023 · 9 comments · Fixed by #1143

Comments

@kbond
Copy link
Member

kbond commented Jul 9, 2023

From #102

Future G) Potentially add support for "lazy" components (they don't load until they are visible) or at least document how one could use a lazy Turbo Frame nicely with a component URL. For example, on page load, a component has a "loading" animation. Then, it loads via Ajax and the area is updated.

Livewire's docs for this feature

@kbond
Copy link
Member Author

kbond commented Jul 9, 2023

A bit of a DX brainstorm:

  1. Should the component class know it's lazy? (livewire does - example stores the loaded state)
  2. Should the component template know it's lazy? (livewire does - example with wire:init)
  3. Should the calling component function mark it as lazy? (livewire does not)

I'm sort of leaning towards a combination of 2 and 3 (opposed to livewire's 1 and 2):

<twig:MyComponent data-defer="true" />
{# MyComponent.html.twig #}

<div {{ attributes }}>
    {% if isInitializing %} {# new attribute provided by live components #}
        Loading... (spinner or maybe a cool animated placeholder skeleton)
    {% else %}
        ... expensive operation ...
    {% endif %}
</div>

On the first render, isInitializing would be true. The js would know to, as soon as the component is initialized on the page, immediately re-render. During this render (and subsequent renders), isInitializing would be false.

At least one problem with this idea: forcing the component to be lazy in the component template (<div {{ attributes }} data-defer="true"> wouldn't work. This would be too late to know to set isInitializing.


I'm very open to other ideas. I was hoping to leverage the current loading state system but I do not think this is possible.

// cc @WebMamba, @sneakyvv any interest in this feature? I'd love some thoughts.

@weaverryan
Copy link
Member

One thing I like about the Livewire way of doing things is that it doesn’t add any new concepts: you create an action, then the only magic is that you ask the action to be automatically executed.

With that in mind, what about something like:

<div {{ attributes.defaults({
    'data-action': 'appear->live#action',
    'data-action-name': 'loadPosts',
}) }}>
<div>

Or, you should be able to equally do this from the outside:

<twig:MyComponent data-action="appear->live#action" data-action-name="loadPosts" />

This takes the normal "action-calling" syntax + Stimulus's normal data-action syntax (which is data-action="EVENT->CONTROLLER#ACTION"). The new addition would be an appear event (name could be changed) that we trigger internally when the element "comes into the viewport. This is modeled off of https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-intersection.md

If you want to simply "load immediately on page load", then instead of appear, I think that this would already work, though the name is a bit cumbersome (the live:connect - we could shorten to connect or load).

<twig:MyComponent data-action="live:connect->live#action" data-action-name="loadPosts" />

@kbond
Copy link
Member Author

kbond commented Jul 10, 2023

Good points. Let's keep it similar to livewire.

What about adding a LazyTrait helper? Something like:

trait LazyTrait
{
    public bool $isLoaded = false;

    #[LiveAction]
    public function load(): void
    {
        $this->isLoaded = true;
    }
}
{# MyComponent.html.twig #}

<div {{ attributes }}>
    {% if not isLoaded %} {# from the trait #}
        Loading... (spinner or maybe a cool animated placeholder skeleton)
    {% else %}
        ... expensive operation ...
    {% endif %}
</div>

Calling:

<twig:MyComponent data-action="appear->live#action" data-action-name="load" />

Maybe we could create a shortcut?

<twig:MyComponent data-lazy />

@weaverryan
Copy link
Member

I like all of this - I'm definitely not against having a shortcut, like data-lazy.

In #996, you mentioned:

One thing we should ensure, if component is lazy and polling is enabled: do not start polling until after the initial load

With this latest iteration, "lazy loading" isn't something the component would be aware of. There would be a convention (via the trait + data-lazy shortcut) that it means that there is some isLoaded property set to true/false, but that's it. The best I can think of is to recommend that people make data-poll conditional:

<div
    {{ attributes }}
    {{ isLoading ? '' : 'data-poll="save"' }}
>

@WebMamba
Copy link
Collaborator

Hum I am not really agree here. Since the laziness of a component is a template concern, only the template should know that the component is lazy.

My proposal is as follow:

<twig:MyComponent data-lazy />

You only have to set this data-lazy attribute, if the data-lazy attribute is set the mount method will be called by ajax when the compose is visible, if not the mount method is call as before.

@weaverryan
Copy link
Member

@WebMamba I think this is valid, but a separate level of laziness. Here are the 2 cases:

A) (existing idea): the component renders, but it has a flag to avoid doing something heavy. That flag changes on load, and then you do the heavy stuff. An advantage of this is that you can easily control the "loading" state - e.g. "Loading...".

B) (your idea): the ENTIRE component is lazy - this is like a lazy <turbo-frame> and would have the highest level of laziness. I like this, but it could be tricky in practice. You would need a way to control the loading state (e.g. "Loading...") and you would need to be able to construct a URL that would render the component, which would come from only the "input props" (as we'd want to avoid actually mounting the component). There is a component_url() Twig function, but that mounts the component then dehydrates it. In this case, you would need to create a URL based off of the "input props", which are not something that we normally need to try to dehydrate. My guess is that we could only reasonably make this work if we required all "input props" to be scalars (e.g. so we could just pop them onto a URL as query params).

@sneakyvv
Copy link
Contributor

I like all the ideas here, but I'm wondering if this is taking the (not yet accepted/merged) live embedded components into account. (#913).
I guess it would work well together since these can also be loaded lazily, and wouldn't be impacted by any of this, right?🤔
Would a component that's being replaced in the DOM again be treated the same and potentially be loaded lazily? Even if it's being replaced within the current viewport?

@weaverryan
Copy link
Member

weaverryan commented Jul 21, 2023

Btw, Livewire 3's lazy feature is like @WebMamba suggested :)

My proposal is as follow:

<twig:MyComponent data-lazy />

You only have to set this data-lazy attribute, if the data-lazy attribute is set the mount method will be called by ajax when the compose is visible, if not the mount method is call as before.

The key thing is that: the component is NOT mounted initially. As I mentioned, this means that you actually need to dehydrate the input props, which is not something that's done anywhere else. But, Livewire does this. Likely, we would need to require the input props to be scalars so that we can just serialize them easily. I think that Livewire does NOT require this, but that means they leak code info (e.g. prop foo is an App\Entity\Foo object) to the frontend, which I think they are generally ok with, but we have avoided thus far.

@sneakyvv

guess it would work well together since these can also be loaded lazily, and wouldn't be impacted by any of this, right?🤔

My guess is that this won't be impacted by your work over there.

Would a component that's being replaced in the DOM again be treated the same and potentially be loaded lazily? Even if it's being replaced within the current viewport?

Not sure what you mean here.

@smnandre
Copy link
Collaborator

I think that Livewire does NOT require this, but that means they leak code info (e.g. prop foo is an App\Entity\Foo object) to the frontend, which I think they are generally ok with, but we have avoided thus far.

I was looking at the code of the LiveRenderer and...

Would it be possible to use an intermediate "proxy/flyweight" ?

Twig -> ProxyComponent -> LongTaskComponent

The "ProxyComponent" would store the data (in backend, file, session... anywhere BUT in the front space) then after some time it would then transmit data to the LongTaskComponent ..

I mean it could fullfill some of the cases you discussed no ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants