How to turn boxcarring OFF for LWC imperative apex method calls?
A 30 second page load is definitely not good. To fix it I recommend stepping back to consider some browser constraints and design patterns that may help.
Browsers limit the number of simultaneous connections they make to a host and the network overall. Generally it’s 4-6 simultaneous connections per host. You can read more here. For per browser stats, see this site.
These connections are used for Aura Actions, LWC @wire
, images, and other assets the app requires. In other words, one quickly runs out of free connections.
Boxcar’ing, which multiplexes multiple requests over a single connection, helps overcome that connection limit. It generally works well but it has its own limits. Eg it serially processes the requests. When you have tens of multi-second requests you run into that limit.
Boxcar’ing or not, one should always consider what constitutes the critical path of their application. Requiring tens of distinct API calls won’t be fast even in a low-latency, high-bandwidth, high-compute browser. A phrase I repeat internally at Salesforce is, The fastest request is the one you don’t make.
There are some patterns you can use to reduce the requests in the critical path. Which you use depends on the use case. Here’s a couple in priority order.
1. Simplify the page
It’s hard knowing what exactly a user needs so one often places everything on the page. A mobile-first design approach forces you to prioritize content because screen real estate is very limited. This same approach should be taken with desktop scenarios. There are many resources online about mobile-first design.
2. Progressive disclosure
Reduce what is in the critical path by not showing non-critical content by default. Use UX patterns like tabsets to “hide” content behind a click and load it lazily. There are many resources online about this pattern.
3. Avoid duplicate requests
Avoid requesting the same content across multiple APIs and ultimately reduce the number of requests. This is often accomplished by requesting the data once from a data manager component and passing it to multiple presentation-only components as props. This approach has other benefits like:
- Simplifying the logic in the majority of components. This makes understanding them simpler, testing simpler, and enables greater reuse
- Grouping API use into fewer components so API transaction boundaries are more appropriate. It also groups API logic so loading and error handling is centralized.
4. Reduce requests
Make fewer requests by fetching all required data in one or a few requests. Sometimes it’s as simple as using a different API that fulfills all your needs, other times it requires more extensive analysis and changes.
5. Caching
Use client-side caching to avoid a trip to the server. Use @AuraEnabled(cacheable=true)
for the built-in client side cache mechanism. If you’ve extremely complex needs you can always build your own client-side cache to tune things.
There are also several ways to cache things on the server to avoid making callouts. One example is the Lightning Platform Cache.
Conclusion
From your description — page has many components, each calls various apex methods, apex makes callouts that take 1-2 seconds each — I believe all of the above approaches will improve the page load time.
Please share the results of your investigation. Many will benefit from your experience.
Now to address your immediate question about boxcar’ing, it’s an implementation detail for how Aura and LWC boxcar requests. This means the implementation may change at any time; you should not program against it unless you’re comfortable with that. To save you from digging through the code (remember LWC is open source), requests queued in the same microtask are boxcar’ed together. You can work around this by using setTimeout
or another call that enqueues across the microtask boundary. But per my note above, the browser has very few simultaneous connections available so it’s trivial to deplete the pool especially in a page composed of many components, let alone those authored by multiple teams and organizations.
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}`);
});
}
I tried investigating effective solution instead of introducing setTimeout as you need certain delay to separate the method from boxcarred method.
Consider below component javascript:
import { LightningElement } from 'lwc'; import getMet1 from '@salesforce/apex/poc.getMet1'; import getMet2 from '@salesforce/apex/poc.getMet2'; import getMet3 from '@salesforce/apex/poc.getMet3'; import getMet4 from '@salesforce/apex/poc.getMet4'; import getMet5 from '@salesforce/apex/poc.getMet5'; import getMet6 from '@salesforce/apex/poc.getMet6'; import getMet7 from '@salesforce/apex/poc.getMet7'; import getMet8 from '@salesforce/apex/poc.getMet8'; import getMet9 from '@salesforce/apex/poc.getMet9'; import getMet10 from '@salesforce/apex/poc.getMet10'; export default class Poc extends LightningElement { connectedCallback() { this.mets(); } async mets() { getMet1().then(data => this.print(data)).catch(err => this.error(err)); getMet2().then(data => this.print(data)).catch(err => this.error(err)); getMet3().then(data => this.print(data)).catch(err => this.error(err)); getMet4().then(data => this.print(data)).catch(err => this.error(err)); getMet5().then(data => this.print(data)).catch(err => this.error(err)); getMet6().then(data => this.print(data)).catch(err => this.error(err)); getMet7().then(data => this.print(data)).catch(err => this.error(err)); getMet8().then(data => this.print(data)).catch(err => this.error(err)); getMet9().then(data => this.print(data)).catch(err => this.error(err)); getMet10().then(data => this.print(data)).catch(err => this.error(err)); } print = data => console.log("print => ", data); error = err => console.error("error => ", err); }
All the 10 calls here are boxcarred into single apex call and so you will see single log with all the logs for 10 apex methods.
Now if I want to separate the first method and run it separately, we can put it in fake promise. This will not create any delay and still manage to have separate apex call.
export default class Poc extends LightningElement {
connectedCallback() {
this.mets();
}
async mets() {
this.setBackground().then(() => getMet1().then(data => this.print(data)).catch(err => this.error(err)));
getMet2().then(data => this.print(data)).catch(err => this.error(err));
getMet3().then(data => this.print(data)).catch(err => this.error(err));
getMet4().then(data => this.print(data)).catch(err => this.error(err));
getMet5().then(data => this.print(data)).catch(err => this.error(err));
getMet6().then(data => this.print(data)).catch(err => this.error(err));
getMet7().then(data => this.print(data)).catch(err => this.error(err));
getMet8().then(data => this.print(data)).catch(err => this.error(err));
getMet9().then(data => this.print(data)).catch(err => this.error(err));
getMet10().then(data => this.print(data)).catch(err => this.error(err));
}
print = data => console.log("print => ", data);
error = err => console.error("error => ", err);
setBackground = () => new Promise(resolve => resolve("success")); // never rejects
}
Now consider separating 2 calls like below:
export default class Poc extends LightningElement { connectedCallback() { this.mets(); } async mets() { this.setBackground().then(() => getMet1().then(data => this.print(data)).catch(err => this.error(err))); getMet2().then(data => this.print(data)).catch(err => this.error(err)); getMet3().then(data => this.print(data)).catch(err => this.error(err)); this.setBackground().then(() => getMet4().then(data => this.print(data)).catch(err => this.error(err))); getMet5().then(data => this.print(data)).catch(err => this.error(err)); getMet6().then(data => this.print(data)).catch(err => this.error(err)); getMet7().then(data => this.print(data)).catch(err => this.error(err)); getMet8().then(data => this.print(data)).catch(err => this.error(err)); getMet9().then(data => this.print(data)).catch(err => this.error(err)); getMet10().then(data => this.print(data)).catch(err => this.error(err)); } print = data => console.log("print => ", data); error = err => console.error("error => ", err); setBackground = () => new Promise(resolve => resolve("success")); // never rejects }
This works perfectly fine if we want to set background upto 4 or 5 calls at a time in single transaction. I am running into boxcarring if I want to separate more than 5 calls like below:
export default class Poc extends LightningElement { connectedCallback() { this.mets(); } async mets() { this.setBackground().then(() => getMet1().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet2().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet3().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet5().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet6().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet7().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet8().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet9().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet4().then(data => this.print(data)).catch(err => this.error(err))); this.setBackground().then(() => getMet10().then(data => this.print(data)).catch(err => this.error(err))); } print = data => console.log("print => ", data); error = err => console.error("error => ", err); setBackground = () => new Promise(resolve => resolve("success")); // never rejects }
Now when we are trying to background all 10 calls, the last 3 or 4 calls gets boxcarred. I think boxcarring logic is somehow readjusting itself with each function call. This has nothing to do with apex according to my investigation.
Conclusion: We can use this logic safely for up to 4 background calls in single transaction. However if anybody finds the exact reason please do share. Thanks!