Skip to content

Commit

Permalink
feature #2463 [Stimulus] Fasten lazy loading + add debug `lazy:loadin…
Browse files Browse the repository at this point in the history
…g` and `lazy:loaded` (smnandre)

This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[Stimulus] Fasten lazy loading + add debug `lazy:loading` and `lazy:loaded`

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Issues        | Fix #...
| License       | MIT

This PR..

1. improves the controller **lazy loading** by tuning algorithm (early exit, remove await, ...)

2. introduces two **new debug events** in the console to ease DX with lazy controllers
    * `my-controller#lazy:loading` when the lazy controller is detected in the DOM
    * `my-controller#lazy:loaded` after the file has been downloaded and imported

I would really like some feedback / tests IRL (especially with Webpack i'm not used to)

Commits
-------

3b8e1ce [Stimulus] Fasten lazy loading + add debug `lazy:loading` and `lazy:loaded`
  • Loading branch information
Kocal committed Jan 16, 2025
2 parents 562c079 + 3b8e1ce commit f1b645e
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 44 deletions.
55 changes: 36 additions & 19 deletions src/StimulusBundle/assets/dist/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,50 @@ class StimulusLazyControllerHandler {
this.lazyLoadNewControllers(document.documentElement);
}
lazyLoadExistingControllers(element) {
this.queryControllerNamesWithin(element).forEach((controllerName) => this.loadLazyController(controllerName));
Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
.flatMap(extractControllerNamesFrom)
.forEach((controllerName) => this.loadLazyController(controllerName));
}
async loadLazyController(name) {
if (canRegisterController(name, this.application)) {
if (this.lazyControllers[name] === undefined) {
return;
}
const controllerModule = await this.lazyControllers[name]();
registerController(name, controllerModule.default, this.application);
loadLazyController(name) {
if (!this.lazyControllers[name]) {
return;
}
const controllerLoader = this.lazyControllers[name];
delete this.lazyControllers[name];
if (!canRegisterController(name, this.application)) {
return;
}
this.application.logDebugActivity(name, 'lazy:loading');
controllerLoader()
.then((controllerModule) => {
this.application.logDebugActivity(name, 'lazy:loaded');
registerController(name, controllerModule.default, this.application);
})
.catch((error) => {
console.error(`Error loading controller "${name}":`, error);
});
}
lazyLoadNewControllers(element) {
if (Object.keys(this.lazyControllers).length === 0) {
return;
}
new MutationObserver((mutationsList) => {
for (const { attributeName, target, type } of mutationsList) {
switch (type) {
case 'attributes': {
if (attributeName === controllerAttribute &&
target.getAttribute(controllerAttribute)) {
extractControllerNamesFrom(target).forEach((controllerName) => this.loadLazyController(controllerName));
for (const mutation of mutationsList) {
switch (mutation.type) {
case 'childList': {
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
extractControllerNamesFrom(node).forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
}
break;
}
case 'childList': {
this.lazyLoadExistingControllers(target);
case 'attributes': {
if (mutation.attributeName === controllerAttribute) {
extractControllerNamesFrom(mutation.target).forEach((controllerName) => this.loadLazyController(controllerName));
}
}
}
}
Expand All @@ -58,9 +78,6 @@ class StimulusLazyControllerHandler {
childList: true,
});
}
queryControllerNamesWithin(element) {
return Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).flatMap(extractControllerNamesFrom);
}
}
function registerController(name, controller, application) {
if (canRegisterController(name, application)) {
Expand Down
67 changes: 42 additions & 25 deletions src/StimulusBundle/assets/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,40 +64,61 @@ class StimulusLazyControllerHandler {
}

private lazyLoadExistingControllers(element: Element) {
this.queryControllerNamesWithin(element).forEach((controllerName) => this.loadLazyController(controllerName));
Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
.flatMap(extractControllerNamesFrom)
.forEach((controllerName) => this.loadLazyController(controllerName));
}

private async loadLazyController(name: string) {
if (canRegisterController(name, this.application)) {
if (this.lazyControllers[name] === undefined) {
return;
}
private loadLazyController(name: string) {
if (!this.lazyControllers[name]) {
return;
}

const controllerModule = await this.lazyControllers[name]();
// Delete the loader to avoid loading it twice
const controllerLoader = this.lazyControllers[name];
delete this.lazyControllers[name];

registerController(name, controllerModule.default, this.application);
if (!canRegisterController(name, this.application)) {
return;
}

this.application.logDebugActivity(name, 'lazy:loading');

controllerLoader()
.then((controllerModule) => {
this.application.logDebugActivity(name, 'lazy:loaded');
registerController(name, controllerModule.default, this.application);
})
.catch((error) => {
console.error(`Error loading controller "${name}":`, error);
});
}

private lazyLoadNewControllers(element: Element) {
if (Object.keys(this.lazyControllers).length === 0) {
return;
}
new MutationObserver((mutationsList) => {
for (const { attributeName, target, type } of mutationsList) {
switch (type) {
case 'attributes': {
if (
attributeName === controllerAttribute &&
(target as Element).getAttribute(controllerAttribute)
) {
extractControllerNamesFrom(target as Element).forEach((controllerName) =>
this.loadLazyController(controllerName)
);
for (const mutation of mutationsList) {
switch (mutation.type) {
case 'childList': {
// @ts-ignore
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
extractControllerNamesFrom(node).forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
}

break;
}

case 'childList': {
this.lazyLoadExistingControllers(target as Element);
case 'attributes': {
if (mutation.attributeName === controllerAttribute) {
extractControllerNamesFrom(mutation.target as Element).forEach((controllerName) =>
this.loadLazyController(controllerName)
);
}
}
}
}
Expand All @@ -107,10 +128,6 @@ class StimulusLazyControllerHandler {
childList: true,
});
}

private queryControllerNamesWithin(element: Element): string[] {
return Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).flatMap(extractControllerNamesFrom);
}
}

function registerController(name: string, controller: ControllerConstructor, application: Application) {
Expand Down

0 comments on commit f1b645e

Please sign in to comment.