10

The accepted answer here Boxcaring is removed from LWC components? contains important information about how requests to the server initiated from LWCs are grouped so browser limits on parallel requests are respected.

We are seeing screens - made up of multiple custom LWCs - where all the components render at the same time after a several second delay and boxcar'ing seems to be the cause. Before doing any refactoring to improve this, it would be great to know:

2

2 Answers 2

9

Are the details of the Boxcaring is removed from LWC components? answer from January 2020 still largely correct - has anything in this area changed?

I wrote an app for this, and I found that all requests are queued up and loaded at once for initial wire methods and connectedCallback calls. Even as few as 2 calls to the server will go together, rather than in parallel. I'm not sure if it's always been this way, or when it changed, but it's here now.

After the connectedCallback for all components has completed, the boxcar effect works as it always has; if there are fewer than 5 requests in-flight, they will go independently, but the sixth slot will be used for boxcar requests.

Here's the original app I used to identify this behavior:

Apex


// Waits one second
public with sharing class q361552 {
    @AuraEnabled(cacheable=false)
    public static void wait(Integer i){
        DateTime start = DateTime.now();
        while(DateTime.now().getTime() - start.getTime()<1000);
    }
}

LWC HTML


<template>
    {message}
</template>

LWC JS


import { LightningElement } from 'lwc';
import wait from '@salesforce/apex/q361552.wait';

export default class Q361552 extends LightningElement {
    message = 'Start loading...';
    connectedCallback() {
        Promise.all(
            [...new Array(2).keys()].map(i => wait({i}))
        ).then(() => this.message = 'Done loading!')
    }
}

AURA APP


<aura:application>
    <c:q361552 />
    <c:q361552 />
</aura:application>

Now, we check the network tab...

Request message


{
  "actions": [
    {
      "id": "1;a",
      "descriptor": "aura://ApexActionController/ACTION$execute",
      "callingDescriptor": "UNKNOWN",
      "params": {
        "classname": "q361552",
        "method": "wait",
        "params": { "i": 0 },
        "cacheable": false,
        "isContinuation": false
      }
    },
    {
      "id": "2;a",
      "descriptor": "aura://ApexActionController/ACTION$execute",
      "callingDescriptor": "UNKNOWN",
      "params": {
        "classname": "q361552",
        "method": "wait",
        "params": { "i": 1 },
        "cacheable": false,
        "isContinuation": false
      }
    },
    {
      "id": "3;a",
      "descriptor": "aura://ApexActionController/ACTION$execute",
      "callingDescriptor": "UNKNOWN",
      "params": {
        "classname": "q361552",
        "method": "wait",
        "params": { "i": 0 },
        "cacheable": false,
        "isContinuation": false
      }
    },
    {
      "id": "4;a",
      "descriptor": "aura://ApexActionController/ACTION$execute",
      "callingDescriptor": "UNKNOWN",
      "params": {
        "classname": "q361552",
        "method": "wait",
        "params": { "i": 1 },
        "cacheable": false,
        "isContinuation": false
      }
    }
  ]
}

And the timing tab confirms that the request took over 4 seconds.

So, adding a small delay, even 100ms, to initial requests will allow for the boxcar effect to work normally (you can use setTimeout for this). You might be able to get even better overall performance if you randomly stagger the requests with setTimeout.

If there are any strategies beyond serious refactoring or using setTimeout (see the answer to How to turn boxcarring OFF for LWC imperative apex method calls?) to alleviate the problem?

There is no real "workaround", just general advice. First, avoid calling imperative Apex in connectedCallback; use setTimeout if you need to. Second, prefer imperative Apex to wire methods. Third, use cacheable methods as much as possible for fewer server calls. Fourth, if wire methods are the best way to do something, make sure you optimize your Apex to use as little resources as possible; while governor limits are per-method-call, these methods do not run in parallel, so will affect overall load time.

2
  • Wow thank-you. This provides a great base to start from.
    – Keith C
    Commented Nov 9, 2021 at 7:48
  • @KeithC No problem, thanks for bringing this to my attention, actually. I learned something new from this.
    – sfdcfox
    Commented Nov 9, 2021 at 15:14
6

Are the details of the ... answer from January 2020 still largely correct?

I can say that our "grid" component (implemented counter to what I suggest in my answer to the second part, with each child requesting its own data) that I showed in my original posting answer still behaves exactly the same - i.e. the first 5 child component requests start immediately and show their results quickly while all the other child components all boxcar and are processed in sequence, having to wait to render until that sequence is complete.

are any strategies beyond serious refactoring (or using setTimeout) to alleviate the problem?

I agree with each of the points @sfdcfox makes for this one, but would add that you should always consider writing your LWCs with the knowledge that boxcarring could happen and, where it makes sense, actually implement client-side orchestration of the (imperative apex) calls so as to:

  1. Initiate the first n (where n should be hard coded as 5) requests "in parallel".
  2. Track the number of requests currently on-going.
  3. When one request finishes, when required, start the next (otherwise decrement the count).

We do this to very good effect to split a large problem domain requiring CPU intensive computation into a sequence of chunks that are processed as much as possible in parallel without using async processes or API calls. We get around a 5 fold improvement in real-time performance (unsurprisingly) for the problem domain processing this way and, of course, can perform way more computation than the typical synchronous 10 CPU second limit.

This same approach can be taken, for example, to avoid the case where dozens of child LWCs each perform a request of their own that results in the first 5 get processed in parallel then all the rest are processed sequentially in a single boxcarred call. Here you could have the children communicate their needs up to the parent which then queues the requests, tracks numbers of parallel calls, submits requests up to (and maintaining) 5 at any time and returns the results to the children, thus avoiding that boxcarring.

Obviously, this orchestration approach is slightly naïve, since the LWCs likely won't be the only ones in the page making calls against the org. However, it still minimizes that conversion into serial request processing over-all.

BTW: I would never recommend using the setTimeout approach to avoid boxcarring as it doesn't consider and adapt to how long individual requests take. Orchestration all the way!

2
  • 1
    Thanks Phil for your excellent ideas on this. There are a lot of redundant server calls in the code (written by an external company) so eliminating those is the first step. And there are also a couple of very expensive calls that hold up the whole UI painting and applying the setTimeout approach there at last results in a 3/4 painted screen with one spinner rather than the whole screen being blocked. But I take your point on using designed in orchestration where you can split the work up and I will keep that notion ready to apply as we dig in further. Thank-you for this & your original post.
    – Keith C
    Commented Nov 9, 2021 at 16:18
  • @KeithC I also added coverage of your first question around my original answer.
    – Phil W
    Commented Nov 9, 2021 at 17:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .