Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 22, 2022 12:35 pm GMT

Surprising Performance Lessons from React Microfrontends in Production

The epilot engineering team stands at 27 developers 1 year after the launch of our rewritten portal built on mostly* React microfrontends.

Microfrontends screenshot

*Part of our app is written using other frontend frameworks, most notably the sidebar navigation written in Svelte.

Since the initial launch a year ago, our teams have gained a lot of experience running React microfrontends in production using single-spa.

While we expected to face challenges with our new frontend microservices architecture, after solving a few initial problems we haven't hit any major snags with single-spa in the first year.

To my surprise, most issues that have emerged in our codebase are general React pain points not specific to microfrontend architecture at all.

In an effort to share knowledge, I'll address the most common React performance antipattern we've observed emerge in our teams in this post.

The state management problem

Here's a really common hook pattern I've seen emerge at one point in most of our React microfrontend projects:

// useFormState.jsximport React from 'react'const FormContext = React.createContext()export const GlobalFormStateProvider = (props) => {  const [formState, setFormState] = React.useState({})  return (    <FormContext.Provider value={{ formState, setFormState }}>      {props.children}    </FormContext.Provider>  )}export const useFormState = () => React.useContext(FormContext)
// App.jsximport { GlobalFormStateProvider } from './useFormState'import { Form } from './Form' export const App = () => (  <GlobalFormStateProvider>    <Form />  </GlobalFormStateProvider>}
// Form.jsximport React from 'react'import { useFormState } from './useFormState'import { api } from './api'export const Form = () => (  const { formState } = useFormState()   const handleSubmit = React.useCallback(    () => api.post('/v1/submit', formState),    [formState]  )  return (    <form onSubmit={handleSubmit}>      <FirstFormGroup />      <SecondFormGroup />    </form>  ))const FirstFormGroup = () => (  const { formState, setFormState } = useFormState()  return (    <div className="form-group">      <input        value={formState.field1}        onChange={(e) =>           setFormState({ ...formState, field1: e.target.value })}      />      <input        value={formState.field2}        onChange={(e) =>           setFormState({ ...formState, field2: e.target.value })}      />    </div>  ))const SecondFormGroup = () => (  const { formState, setFormState } = useFormState()   return (    <div className="form-group">      <input        value={formState.field3}        onChange={(e) =>           setFormState({ ...formState, field3: e.target.value })}      />    </div>  ))

Many readers will immediately recognize antipatterns in the above example, but entertain the nave perspective:

The useFormState() hook is very useful. No prop drilling. No fancy global state management libraries needed. Just native React.useState() shared in a global Context.

What's not to love here?

The problems

As nice as useFormState() seems, we'd quickly face performance issues due to components using it having to render on every setFormState() causing unnecessary, potentially expensive re-renders.

This is because we've subscribed all our Form components to re-render on all changes in FormContext by using React.useContext(FormContext) inside useFormState().

You might think React.memo to the rescue, but reading the React docs:

When the nearest above the component updates, this Hook will trigger a re-render with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a re-render will still happen starting at the component itself using useContext.

Further, we're unnecessarily depending on the full formState object in all our form components.

Consider:

// formState is a dependency:setFormState({ ...formState, field1: e.target.value })}// formState not a dependency:setFormState((formState) => ({ ...formState, field1: e.target.value }))

At this time, I would consider Context Providers using React.useState to store complex global app state a general performance antipattern.

However, if React adds useContextSelector (RFC) I am positive the situation could change.

Lessons learned

Seeing antipatterns like these emerge in React projects even with fairly experienced frontend developers (think 5+ years of React) has lead me to consider performance as a topic that unfortunately requires pretty significant investment to produce quality output when working with React in general.

As always, there is No Silver Bullet. However, our frontend microservices architecture has enabled us to cheaply experiment with different approaches in different teams who have produced quite a few competing strategies to solve form performance:

Additionally, thanks to the flexibility of single-spa we've been able to experiment outside of the React ecosystem with frameworks like Svelte and others which has been extremely promising and rewarding for our engineers.

epilot logo

We're hiring @ epilot!


Original Link: https://dev.to/epilot/surprising-performance-lessons-from-react-microfrontends-in-production-528a

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To