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}`);
});
}
The LWC framework (i.e. everything you need to make LWC work outside of Salesforce) is open-source. The individual bits that only work inside the Salesforce-hosted LWC (including Lightning Out, etc) are not open source. This is also true for Aura; the framework itself is open source, but the "secret sauce" (e.g. anything in ui:
, force:
and lightning:
) are not open source.
As such, if you want to look at the source code for those components, you're left to using the browser's Developer Console (F12). As long as you're in Debug Mode, you can browse the framework source in the Developer Console.
As a special note, sometimes the Specification tab is indeed deficient in describing how to set some attributes. In this case, the Documentation tab should contain all the relevant rules. As a backup plan, you can look at the documentation for the Aura version of the same component. It is sometimes more complete, although be aware that differences between Aura and LWC components may exist.
Best Answer
Feedback from Salesforce core engineering team
Added by @salesforce-sas and updated by @PhilW:
There were major design changes rolled out in Winter '20 affecting the way server calls are made. Everything in LWC are separate parallel calls to the server at the Lightning Data Service (LDS) layer. However, the boxcarring effect kicks in at the Aura Data Service (ADS) layer (currently the intermediary between LDS and the server) when all available parallel XMLHttpRequests (XHRs) from the browser to the server are busy.
Currently all browsers support 6 parallel XHRs as standard. When ADS has started 5 concurrent XHRs, the 6th (last one) boxcars and sends all the remaining server calls queued by the LDS. This is the reason that, although we see only 6 XHRs in browser network tab, we will see individual apex logs per request in the Salesforce Developer Console.
This is not documented because the Salesforce dev team is continuously trying to improve server calls (LDS) and this design/implementation is not the final one. It is doubtful that these design changes will ever be documented.
We haven't seen any changes to boxcarring, at least in the last 6-7 months from when we started looking at this. There are specific details on how LEX and LWC are working that haven't been explained anywhere that I've found which may be affecting what you're seeing.
If you load a page with a large number of components in it that do a lot of different wire or imperative calls you will see that the way the LEX/LWC infrastructure works is:
Here's an example. We have a grid (component). The grid is populated with visual representations of SObjects (component). Each cell (component) in the grid performs its own (here imperative) call to the server to fetch its data then uses that to create the SObject representations. Once completely rendered it looks like this:
However, when the grid is first being rendered in the LEX Object Record Page, we see:
You can see that only 5 cells have executed their calls and been rendered; all the other cells are still waiting. Once the waiting has finished, the next thing the user sees is basically the completed picture (first figure, above).
If you look at the network traffic you see the following:
You can see that the first 5 wire/imperative calls are allowed to run in parallel. Once one of these has finished (so there is an available "thread" from the LEX perspective) you then see that almost all the other requests get collected together and run as a single invocation to the server - this is the boxcarring in action.
** It is worth noting that the various browser implementations actually limit the number of "parallel" AJAX calls to the same host. For Chrome it is 6. This may mean you see different behaviour on different browsers. Take a look here for some further details.
To conclude - boxcarring is still there, but if you keep your number of "start at or around the same time" calls to 5 or less no boxcarring will happen. We use this to good effect in some of our other components where we specifically orchestrate our (imperative) calls to the server to do 5 or less at any one time. Other requests get postponed and are started when an existing request completes.
PS: One thing that did change recently - boxcarred requests now correctly each get their own limits. This wasn't the case previously due to a bug in the platform.