Read more at, or return to the homepage

React Versus Lightning Web Components

I'm in the midst of writing a "back to basics" post on object-oriented programming, which has been a popular request for some time now. I was de-railed while working on that post after observing some other people talking about Lightning Web Components (LWC), the new-ish Salesforce frontend framework that has rapidly been pushed out the door to phase out the badly-aging Aura framework upon which Salesforce Lightning was first designed. LWC itself is based upon the open-source Web Components standard, care of the great people at Mozilla.

On the backend, I come from a .Net / Java background, which made diving into Apex fairly easy. On the frontend, I worked on some old Backbone / Handlebars / Angular codebases, prior to making the jump into developing websites with React. By the time LWC came out, React had already begun favoring the usage of function components over class-based components, but the similarities were certainly there between the two. In truth, LWC shares more similarities to Vue.js than it does to React, but according to the 2019 State Of JavaScript, React is still the frontend framework leader in terms of market-share. Comparisons between LWC and React are inevitable as a result.

Doing a little cross-comparison between the two frameworks seems like a fun exercise; specifically, I'm curous to see if there are significant performance differences between LWC and React.

linkTesting In A React/Typescript Component

This sample comes from a Typescript/React codebase for a client who needed to display an FAQ section on their site. If you're not familiar with Typescript/React, don't worry! Take a skim through and see if you recognize anything (or simply scroll through). See if you facepalm, as I did after re-reading what I'd written:

1linkimport React, { FC, Reducer, useReducer } from "react";


3linktype Action = {

4link type: string;



7linktype FAQ = {

8link answer: string;

9link question: string;



12linktype FAQProps = {

13link faq: FAQ[];



16linktype State = {

17link [key in string]: boolean;



20linkconst getInitialState = (faq: FAQ[]) =>

21link faq

22link .map((frequentlyAsked) => ({

23link name: frequentlyAsked.question,

24link isExpanded: false,

25link }))

26link .reduce(

27link (previousValue, currentValue) => {

28link return {

29link ...previousValue,

30link []: currentValue.isExpanded,

31link };

32link },

33link { base: false }

34link );


36linkconst reducer: Reducer<State, Action> = (

37link state: State,

38link action: Action

39link): State => {

40link const isExpanded = !state[action.type];

41link return { ...state, [action.type]: isExpanded };



44linkconst HideableSpan = ({ isExpanded }) => {

45link const transformStyle = `rotate(${!!isExpanded ? 90 : 0}deg)`;

46link return (

47link <span

48link style={{

49link display: "block",

50link marginLeft: "-15px",

51link position: "absolute",

52link top: 0,

53link transform: transformStyle,

54link transition: `ease 0.3s`,

55link }}

56link >


58link </span>

59link );



62linkexport const FAQ: FC<FAQProps> = ({ faq }) => {

63link const initialState = getInitialState(faq);

64link const [state, dispatch] = useReducer(reducer, initialState);


66link return (

67link <section>

68link <h1>FAQ</h1>

69link { => (

70link //style omitted for brevity

71link <div

72link key={frequentlyAsked.question}

73link onClick={() =>

74link dispatch({

75link type: frequentlyAsked.question,

76link })

77link }

78link >

79link <h2>

80link {frequentlyAsked.question}

81link <HideableSpan isExpanded={state[frequentlyAsked.question]} />

82link </h2>

83link //styling not displayed for brevity //but similar to the HideableSpan

84link <small isExpanded={state[frequentlyAsked.question]}>

85link {frequentlyAsked.answer}

86link </small>

87link </div>

88link ))}

89link </section>

90link );


This is a pretty minimal component that takes in a list of frequently asked questions and makes use of the lazily-loaded useReducer pattern for creating initial state, where all of the questions are mapped to a state object with the question used as a key and a boolean representing their expanded value. Clicking on the question shows the answer — clicking again re-hides the answer. Fairly standard stuff for React. Probably the only notable thing about this example is the use of useReducer in general, because the simplest possible solution in React would be to useState ... except that here, that would already be pretty complicated, because we don't know how many questions are going to be loaded into this component to begin with.

Even though this is a relatively simple piece of code, it's also a mess. Most of the mess is contained within getInitialState(), which is essentially flattening the list of frequently asked questions into an object, as described. Why is it a mess? It's hard to follow, particularly the call to the native JS function reduce. It compiles and it works, but it's also totally inscrutable, particularly if anybody other than me should ever work on this codebase. What the hell is that { base: false } section even doing, for example?

Let's look at how that getInitialState function might be suitably ... reduced to something more understandable:

1linkconst getInitialState = (faq: FAQ[]) =>

2link faq

3link .map((frequentlyAsked) => ({

4link [frequentlyAsked.question]: false,

5link }))

6link .reduce((previousValue, currentValue) => ({

7link ...previousValue,

8link ...currentValue,

9link }));

Now we're starting to get somewhere. Most people would probably take this as an acceptable solution and move on. But, to quote Mr. Money Mustache on the subject of coffee ... "how much is that bitch costing ya?". People love love love to use .map and .forEach and reduce in JS, without regards to performance — and, if this component were to be frequently re-rendered with a suitably large list of questions, iterating twice over the list of FAQs could indeed become a performance bottleneck (not to mention mapping over the values once more in the actual render method of the component). It's something that should at least be measured, so let's create an even faster FAQ component and use Jest + Enzyme to measure the costs of both components updating:

1linkexport type FAQItem = {

2link answer: string;

3link question: string;



6linktype FAQProps = {

7link faq: FAQItem[];

8link getInitialState?: (faq: FAQItem[]) => State;





13linkexport const FAQ: FC<FAQProps> = ({ faq, getInitialState }) => {

14link const initialState = !!getInitialState

15link ? getInitialState(faq)

16link : getInitialStateDefault(faq);

17link //..


You could get much crazier than that, and I considered it, but sometimes simple is best. Let's look at how these tests shape up:

1linkimport { mount } from "enzyme";

2linkimport React from "react";


4linkimport { FAQ } from "../faq";

5linkimport { FastFAQ } from "../fast-faq";


7linkconst getList = () => {

8link const list = [];

9link for (let index = 0; index < 1000; index++) {

10link list.push({ question: index, answer: "some answer" });

11link }

12link return list;



15linkconst bigOldList = getList();


17linkconst fastInitialState = (args: FAQItem[]) => {

18link const initialState = {};

19link for (let index = 0; index < args.length; index++) {

20link const frequentlyAsked = args[index];

21link initialState[frequentlyAsked.question] = false;

22link }

23link return initialState;



26linkdescribe("FAQ Performance", () => {

27link it("should render faq", () => {

28link const wrapper = mount(<FAQ faq={bigOldList} />);

29link wrapper.update();

30link });

31link it("should render fast-faq", () => {

32link const wrapper = mount(

33link <FAQ faq={bigOldList} getInitialState={fastInitialState} />

34link );

35link wrapper.update();

36link });


And the results?

1link$ FAQ Performance

2link$ √ should render faq (200ms)

3link$ √ should render fast-faq (145ms)


5link$Test Suites: 1 passed, 1 total

6link$Tests: 2 passed, 2 total

Not too shabby. Even when the list of questions was "only" 100 questions long (and keeping in mind the fact that longer answers would almost certainly negatively impact re-rendering performance), the difference between re-renders was consistently at least 50ms. UX research has suggested that as little as 50ms in delay can negatively affect users, both in terms of conversion and (for internal portals) assignment completion.

In short, small delays in rendering/re-loading pages quickly add up; as developers, it shouldn't only be our mandate to make things work, but to make things work well. Knowing about relative performance, particularly when branching out into LWC development — where the JavaScript you write is going to have to be processed client-side for your users — is not only important for you as the programmer when you need to return to code, but because most users operate on substandard hardware that chug on huge quantities of JS and CSS.

One thing that's really pleasant, as a developer, when working with the above code, is how testable the implementation is. You can easily, for example, test that the implementation of getInitialState in FAQ creates state predictably, with a minimum of fuss — all you need to do is pass in a list of objects with question and answer properties to validate that things are working as anticipated. Furthermore, thanks to the power of Jest + Enzyme (not to mention the whole host of other React testing libraries out there), we can actually examine the internals of the rendered component to validate that, when a particular question is clicked, the HideableSpan component actually rotates the arrow, and the text of the answer is exposed.

At a high level, this is precisely why React has gained the prominence it now has within the frontend community — by splitting the traditional MVC architecture into discrete, testable presentation pieces, React encourages code re-use and abstraction at a similar level to the foundations of object-oriented programming represented by a language like Apex.

linkTesting In A Lightning Web Component

Let's make the jump and look at this same problem, this time within a LWC.

There are many, many ways to skin this particular cat, highlighting both the strengths and weaknesses of LWC in a nutshell. In order to ingest the FAQ question/answer pairs, for example, you could use @wire to fetch data from a custom object using an Apex controller. You could use @api and getter/setter methods to control the isExpanded property that we need to track in order to expand/contract the FAQ question to show the answer.

Here's the most basic implementation for the LWC:

1linkimport { LightningElement, wire } from "lwc";

2linkimport getFAQs from "@salesforce/apex/FAQController.getFAQs";


4linkexport default class FAQList extends LightningElement {

5link @wire(getFAQs) faqs;

6link activeSections = [];


And the Apex controller:

1linkpublic class FAQController {

2link @AuraEnabled(cacheable=true)

3link public static List<FAQ> getFAQs() {

4link List<FAQ> faqs = new List<FAQ>();

5link for(Integer index = 0; index < 100; index++) {

6link String indexString = String.valueOf(index);

7link faqs.add(new FAQ('Question for ' + indexString, 'Answer for' + indexString, indexString));

8link }

9link return faqs;

10link }


12link public class FAQ {

13link public FAQ(String question, String answer, String key) {

14link this.question = question;

15link this.answer = answer;

16link this.key = key;

17link }

18link public String question { get; private set; }

19link public String answer { get; private set; }

20link public String key { get; private set; }

21link }


And the LWC markup:


2link <template if:true="{}">

3link <template for:each="{}" for:item="faq">

4link <lightning-accordion

5link allow-multiple-sections-open

6link key="{faq.key}"

7link active-section-name="{activeSections}"

8link >

9link <lightning-accordion-section name="{faq.key}" label="{faq.question}">

10link <small>{faq.answer}</small>

11link </lightning-accordion-section>

12link </lightning-accordion>

13link </template>

14link </template>


And the tests:

1linkimport { createElement } from "lwc";

2linkimport { registerApexTestWireAdapter } from "@salesforce/sfdx-lwc-jest";


4linkimport FAQ from "c/faq";

5linkimport getFAQs from "@salesforce/apex/FAQController.getFAQs";


7linkconst FAQ_AMOUNT = 1000;


9linkconst getFakeFAQs = () => {

10link const faqs = [];

11link for (let index = 0; index < FAQ_AMOUNT; index++) {

12link faqs.push({

13link question: "test question" + index,

14link answer: "test answer " + index,

15link isExpanded: false,

16link key: index,

17link });

18link }

19link return faqs;



22linkconst getFAQAdapter = registerApexTestWireAdapter(getFAQs);


24linkfunction assertForTestConditions() {

25link const resolvedPromise = Promise.resolve();

26link return resolvedPromise.then.apply(resolvedPromise, arguments);



29linkdescribe("FAQ", () => {

30link afterEach(() => {

31link while (document.body.firstChild) {

32link document.body.removeChild(document.body.firstChild);

33link }

34link jest.clearAllMocks();

35link });


37link describe("FAQ tests", () => {

38link it("renders full faq list", () => {

39link const element = createElement("faq-list", {

40link is: FAQ,

41link });


43link document.body.appendChild(element);

44link getFAQAdapter.emit(getFakeFAQs());


46link return assertForTestConditions(() => {

47link expect(

48link element.shadowRoot.querySelectorAll("lightning-accordion-section")

49link .length

50link ).toBe(FAQ_AMOUNT);

51link });

52link });


54link it("expands and contracts on click", () => {

55link const element = createElement("faq-list", {

56link is: FAQ,

57link });

58link document.body.appendChild(element);

59link getFAQAdapter.emit(getFakeFAQs());


61link //get the first anchor and test clicking it

62link assertForTestConditions(

63link () =>

64link element.shadowRoot

65link .querySelector("lightning-accordion-section")

66link .click(),

67link () =>

68link expect(element.shadowRoot.querySelectorAll("small").length).toBe(1)

69link );

70link });

71link });


This leads to the following output:

1link$ FAQ

2link$ FAQ tests

3link$ √ renders full faq list (415ms)

4link$ √ expands and contracts on click (430ms)


6link$Test Suites: 1 passed, 1 total

7link$Tests: 2 passed, 2 total

You'll notice that I didn't include the test for expanding/contracting when reviewing the React version of this component. This is partially because I expect readers to be less familiar with Typescript/React, and because that discussion was more about how simple JavaScript enhancements can lead to big increases in render speed. Here, the conversation is a little different:

A few takeaways:

With those things being said, it's my hope that the @salesforce/sfdx-lwc-jest library will continue to expand, offering developers better options for feeding their LWC data

linkRevisiting React To Test Clicking

Let's re-visit the Typescript/React implementation, swapping out the heavy-lefting mount call to Enzyme with the more-performant shallow renderer (to try to get closer to an apples-to-apples comparison between the two frameworks). I'll add in a test for clicking one of the FAQ questions, with the caveat that normally I would be using a CSS-in-JS library and actually using helper functions to assert that the styles matched after clicking. This is an important distinction between ecosystems, however — it's easy to use soomething like Emotion or Styled Components with React and then verify that styles have updated correctly using helper functions provided by those self-same libraries.

This is also exactly what I am hoping will happen within the LWC community — it would be awesome to see something like hot module reloading (HMR) when developing LWC, for example, and HMR was an open-source contribution within the ecosystem of frameworks that uses Webpack to bundle their code ... so it's certainly possible.


1linkimport { shallow } from "enzyme";

2linkimport React from "react";


4linkimport { FAQ } from "../faq";

5linkimport { FastFAQ } from "../fast-faq";


7linkconst listCount = 1000;


9linkconst getList = () => {

10link const list = [];

11link for (let index = 0; index < listCount; index++) {

12link list.push({ question: index, answer: "some answer" });

13link }

14link return list;



17linkconst bigOldList = getList();


19linkdescribe("FAQ", () => {

20link it("should render faq", () => {

21link const wrapper = shallow(<FAQ faq={bigOldList} />);

22link expect(wrapper.getElements()[0].props.children[1].length).toBe(listCount);

23link });

24link it("should render fast-faq", () => {

25link const wrapper = shallow(<FastFAQ faq={bigOldList} />);

26link expect(wrapper.getElements()[0].props.children[1].length).toBe(listCount);

27link });

28link it("should toggle isExpanded on click", () => {

29link const wrapper = shallow(<FastFAQ faq={bigOldList} />);

30link wrapper.getElements()[0].props.children[1][0].props.onClick();

31link expect(

32link wrapper.getElements()[0].props.children[1][0].props.children[1].props

33link .isExpanded

34link ).toBeTruthy();

35link });


This leads to the following output:

1link$ FAQ

2link$ √ should render faq (235ms)

3link$ √ should render fast-faq (61ms)

4link$ √ should toggle isExpanded on click (97ms)


6link$Test Suites: 1 passed, 1 total

7link$Tests: 3 passed, 3 total

When testing components as their own strict unit (which is what shallow does within Enzyme, as opposed to simulating the load of the whole DOM), there's no denying that React can be absurdly fast. I've worked on large React applications, and have seen firsthand how well the framework can scale.

linkWrapping Up: React Versus Lightning Web Components

Some people are sidestepping the whole question of which framework is better by embedding React applications into their Lightning application pages.. I ... probably wouldn't recommend doing that.

I feel as though I am still holding out on LWC. On the one hand, I like how composable the framework can be through the use of <slot> components and the <c-component-name> binding. On the other, I still prefer the explicit binding between imported components in React; it's one less step to see where all of your components are being used — but perhaps at some point there will be "View Usage" functionality for LWC.

Rendering performance with LWC still leaves something to be desired. While certainly faster than the components that were being built out with Aura (and I don't miss those days at all), LWC still feels relatively sluggish compared to components rendered with Vue or with React. I would be interested in hearing about other performance optimizations that people have found helpful in their LWC usage.

How do you feel about LWC? Was this comparison with React interesting to you? Let me know in the comments! I've seen some pretty impressive components built out already, and I'm looking forward to building more and seeing the framework achieve mainstream adoption and better tooling support within the SFDC ecosystem. If you'd like to peruse the code from this example, please refer to my GitHub's LWC branch.

Stay tuned for my Apex Object-Oriented Programming Basics post. Till then, thanks for reading - if you liked this post, check out my post on creating a composable pagination component!

The original version of Lightning Web Components: React Versus LWC can be read on my blog.

Testing In A React/Typescript ComponentTesting In A Lightning Web ComponentRevisiting React To Test ClickingWrapping Up: React Versus Lightning Web Components

Home Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Formula Date Issues Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Replacing DLRS With Custom Rollup Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests

Read more tech articles