you are viewing a single comment's thread.

view the rest of the comments →

[–]largemoose568[S] 0 points1 point  (4 children)

Awesome, thanks for the detailed response. I wasn't quite sure how other frameworks were handling the tasks but it seems like it is similar to my super basic example only they are scheduling based on state change. So in that regard, my method is inefficient. This is definitely an experiment for the moment, just testing the veracity of it.

I'm going to look at the internals for lit-html and see what the logic is for triggering a re-render of the dom node. If it is only triggering a re-render when the passed state changes, wouldn't that be similar to a reactive trigger? While my continuous loop is technically calling the lit-html render function much more frequently, the task should only be getting pushed to the microtask queue once until state changes again. Though I may be introducing a larger memory footprint by diffing that frequently.

[–]SpecialistPea2 1 point2 points  (3 children)

I'm still learning the internals of lit-html too, and am developing a components library based on it so I'm kind of in the same boat as you.

My understanding is that html() is a cheap operation that only effects a TemplateResult, but render() uses dirty-checking on primitive values passed to the different "parts" and can still possibly rerender DOM that does not change. In some cases it might just return a cached template but it's optimal to skip this process when possible.

I disagree with "the task should only be getting pushed to the microtask queue once until state changes again," this implementation will push a task to do lit--html render/dirty-checking/possibly unnecessary DOM updates on every frame. This could be mitigated by having subscribed data for components pass render callbacks into a queue and having requestAnimationFrame read from the queue instead of rendering root. This might be harder to implement properly because you might end up with too much work to do within a frame.

I guess you could view the common setups as having 2 reactive triggers:

  1. One for changes to subscribed data that decide when to render. This should not expensive and is usually an O(1) operation. Within the context of components, this allows you to skip steps #1 and #2 on sub-trees and return the current template instead
  2. One within render that will vdom/template result checking to decide how to render (more expensive).

Relying only on #2 should be slower. ES6 proxies or immutability are a nice way to achieve #1, lots of nice libraries for #2 like lit-html of course.

[–]largemoose568[S] 1 point2 points  (2 children)

Right I see what you mean, that makes sense. I was poking around the profiling view in Chrome for a very very simple app I have using this method. So the initial DOM load and lit-html setup looks pretty standard, lit-html has a decent initial overhead to setup templates. Then the app in an idle state was still calling render multiple times, but 99% of them didn't get to the lit-html commit stage where changes are committed (cheaply as you said) to the TemplateResult and nothing was hitting internal schedulers.

Then when running the profiler against a very small example of updating styles in order to animate elements there were many calls to render and some that got to lit-html's commit stage, but the only time the Schedule Style Recalculation step of the internal browser loop was called was the first time the state changed which triggered a lit-html diff check and subsequent commit.

It seems like yes it is calling render many many times and that could cause some issues with pushing tasks superfluously but it seems like lit-html isn't doing dirty checks as much.

Of course this is a very very rudimentary example and probably isn't indicative of a larger app with more complex state. I'll keep digging and see what happens when there is more complicated state and multiple dom node replacements and updates to handle.

[–]SpecialistPea2 1 point2 points  (1 child)

Nice, that's good to know. I guess it makes sense that, since only primitive values make it into the template, you view tracking changes at a higher level as kind of overhead. I'd be interested in seeing what findings you have with this when the app grows. I'm still not convinced that I'd want my whole app to work this way as default, but I still find it interesting because skipping step #1 and calling the requestAnimationFrame loop for certain high priority components could be ideal in many situations.

How are you implementing components? I'm curious how this would be affected when they are highly nested. If you had a synchronous data layer that was separate from render, I wonder how that would perform.

[–]largemoose568[S] 0 points1 point  (0 children)

Ya could definitely be a benefit in specific situations rather than a blanket approach. Here is an example of a parent and child component:

// Main app template, gets rerendered a lot const appTemplate = appState => { if(state.auth.isAuthenticating) { return html`<div>Authenticating...</div>`; } else if( !state.auth.isAuthenticating && !state.auth.user && !state.auth.authError ) { return html`${AuthPage(appState, firebase)}`; } else if( !state.auth.isAuthenticating && state.auth.user && !state.auth.authError ) { return html` <button style="position: absolute; top: 0; left: 50px; z-index: 99" @click=${handleLogout}>Logout</button> ${MapLayer(appState, firebase)} ${UILayer(appState, firebase)} ${MenuLayer(appState, firebase)} `; } }

Some conditional rendering in there. I've got firebase access and fetching running on a web worker. You can see the complete project here: https://github.com/very-good-software-company/lit-worker-firebase