alpinejs / alpine

A rugged, minimal framework for composing JavaScript behavior in your markup.
https://alpinejs.dev
MIT License
28.46k stars 1.24k forks source link

Add an option to configure the MutationObserver #4286

Open imliam opened 4 months ago

imliam commented 4 months ago

Problem:

We are using Alpine on a website with a large DOM and many nodes changing from third-party scripts that we don't have control over. We've found that Alpine's MutationObserver causes problems when watching the entire document, notably:

  1. An increase in INP and CPU usage, as Alpine's mutation callback triggers a lot
  2. Race conditions with other scripts modifying the DOM

Solution:

The option added in this PR lets us call Alpine.start(false) with the false flag, causing the MutationObserver only to watch [x-data] elements instead of the entire document.

Of course, this means some functionality will not work as intended when a mutation happens outside an Alpine component. Still, for a page where each Alpine component only cares about its contents, this has a huge performance benefit with no real downsides.

I'm not sure if an option in Alpine.start() is preferable of there should be some other way to configure this, but the option seems to work for our use case.

ekwoka commented 4 months ago

We talked about this a bit on Discord, but I'll repeat/elaborate here for the discussion.

As Rich Harris recently said

Configuration is cowardly — it removes the burden of designing something correctly off maintainers' shoulders and puts it on users instead

So is there a solution to this issue that doesn't involve a configuration option?

The goal of the mutation observer is to

It also then watches the whole document (and not just known components) so as to

The performance issues mainly come from the fact that every element added/moved/removed is processed as if it is an alpine component, even when it isn't. Currently, this also actually allows for a bug where alpine attributes that require a context can be on element added to the page outside of a component, and they'll be initialized improperly.

So I think there is room to tackle the performance issues, while maintaining both the above behaviors.

Mainly two observers:

  1. Watches for all attribute/element changes inside a component
  2. Watches for new top level x-data (and x-init) outside components

Having these two separate can reduce how often the handlers are invoked, and reduce the overhead of processing completely non-alpine mutation entries.

How much exactly this would improve performance, I'm not totally sure, but I know initializing a tree where none exists is definitely far too costly.

I believe currently, x-ignore will prevent the subtree from being initialized when the walker gets to it, but will not stop the mutation observer from watching inside of it. I might even go so far as to say that's a bug. That elements added within an x-ignore should also be ignored by the mutation observer. This would allow some declarative user land tuning of the observers scope.

calebporzio commented 1 month ago

Yeah, I think I agree with these points @ekwoka. Ideally we find a way to not unnecessarily tax mutations. I'd be interested in a deeper discussion on this with some benchmarks and what not. Maybe something to plan on tackling for Alpine 4