You have two different questions here, but I will try to answer this one:
But what about the initial run? Do wired methods always run when the page loads?
That is correct. I myself was trying to identify as when does the wired method get invoked. And based on a test I came to the conclusion that it always gets invoked the time the component is being created.
However the methods need to be annotated with @AuraEnabled(cacheable=true)
, because if not, then you will need to invoke it imperatively.
I have a small test around this topic and its results here.
As for your other question on hierarchical components, if you want to the queries to be fired on demand, then its better to go the imperative approach.
First--very well constructed question, and good investigation.
I was not aware of this issue with boxcarring in LWC.
I'm going to focus on a workaround rather than an actual setting, since I'm sure you've searched for that already.
What happens if you put your apex invocations inside setTimeout calls?
I know it's needlessly adding time, but you could add small delays like 50 msec or possibly even 0 mSec just to throw it on the stack.
The idea here is that Salesforce Lightning would have no place to gather all the simultaneous calls in one hidden object only to submit them all at once. When the active thread is building the page with your components, it's all happening in one thread.
Each imperative call is captured for a subsequent boxcar call.
However, if you start stacking calls, I don't see how boxcarring could intervene.
The initial thread would run to execution, and then presumably the boxcar thread would be called, and finally your setTimeouts.
I'm very anxious to hear if this approach works.
Update: Mixed results
I tried this out and given any number of apex method callouts, this approach un-boxed the first one or two callouts, but then all the rest got boxed up again.
This obviously made the biggest difference if the first callout was the longest, but without my code, all of the callouts ALWAYS were serially boxed.
Now, as it turns out delaying the call with the embedded setTimeout didn't cause this effect. It seems that simply calling a separate then-able ("sleeper()") in the Promise handler method was enough to disrupt the boxcarring of at least the first couple of apex callouts, regardless of whether there was an active setTimeout call.
Conclusion: This approach can definitely disrupt the boxcarring of the first two apex callouts, but is probably not useful since all the others remain boxed up. A more reliable solution may be to execute the callouts from Lightning/Javascript rather than via the Apex methods.
Here's the console log when each of the 4 callouts was set to a 1 second delay:
Call 1 Elapsed =1360
Call 2 Elapsed =1379
Call 3 Elapsed =2515
Call 4 Elapsed =2515
Total Elapsed =2515
Here's the console when with the longest calls starting first:
Call 2 Elapsed =3361 (3 second call)
Call 3 Elapsed =3527 (2 second call)
Call 4 Elapsed =3528 (1 second call)
Call 1 Elapsed =4354 (4 second call)
Total Elapsed =4354
In this best-case example, the shortest 2 calls were boxed up giving us the best possible improvement.
Here's the relevant code:
sleeper(ms) {
if (this.background === true) {
console.log('background=true');
return function (x) {
return new Promise(resolve => setTimeout(() => resolve(x), ms));
};
} else {
console.log('background=false');
return Promise.resolve('hello');
}
}
connectedCallback() {
console.log(this.startTime);
Promise.all( [
Promise.resolve('hello').then(()=> this.sleeper(1)).then(()=> requestWithSleep({sleepSeconds : 4})).then( ()=> console.log(`Call 1 Elapsed =${Date.now() - this.startTime}`)),
Promise.resolve('hello').then(()=> this.sleeper(1)).then(()=> requestWithSleep({sleepSeconds : 3})).then( ()=> console.log(`Call 2 Elapsed =${Date.now() - this.startTime}`)),
Promise.resolve('hello').then(()=> this.sleeper(1)).then(()=> requestWithSleep({sleepSeconds : 2})).then( ()=> console.log(`Call 3 Elapsed =${Date.now() - this.startTime}`)),
Promise.resolve('hello').then(()=> this.sleeper(1)).then(()=> requestWithSleep({sleepSeconds : 1})).then( ()=> console.log(`Call 4 Elapsed =${Date.now() - this.startTime}`)),
])
.catch(error => {
console.log('error loading page data:');
console.log(error);
})
.finally(() => {
console.log(`Total Elapsed =${Date.now() - this.startTime}`);
});
}
Best Answer
Wire methods take care of the entire life cycle of an attribute when it changes, including fetching calling the method, updating the attribute, and and refreshing the UI, all in often a single line of code (or just a few for custom error handling). While you could do all this imperatively, it would require significant amounts of code. In other words, yes, wire methods are basically just a "shortcut" for calling Apex imperatively, but you should not discount those savings, which could easily be hundreds of lines of code in a large component. Also, specifically, the wired method approach means you don't need to write custom setters to make sure the method is called every time the attribute is updated, or remember to call the method imperatively every time. This centralizes calling the method to a single location, making it much less likely you'll forget to call the method when appropriate.